Merge platform/external/python/google-auth-library-python v2.3.3 am: 8c673285f7 am: ca3e33f7be

Original change: https://android-review.googlesource.com/c/platform/external/python/google-auth-library-python/+/1930534

Change-Id: I4efa7e6b9e5e815be712ad6071d94b9b373ab77f
diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..9ba3d3f
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,15 @@
+[run]
+branch = True
+
+[report]
+omit =
+    */samples/*
+    */conftest.py
+    */google-cloud-sdk/lib/*
+exclude_lines =
+    # Re-enable the standard pragma
+    pragma: NO COVER
+    # Ignore debug-only repr
+    def __repr__
+    # Don't complain if tests don't hit defensive assertion code:
+    raise NotImplementedError
diff --git a/.flake8 b/.flake8
new file mode 100644
index 0000000..0574e0a
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,8 @@
+[flake8]
+ignore = E203, E266, E501, W503
+exclude =
+  # Standard linting exemptions.
+  __pycache__,
+  .git,
+  *.pyc,
+  conf.py
diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml
new file mode 100644
index 0000000..cb89b2e
--- /dev/null
+++ b/.github/.OwlBot.lock.yaml
@@ -0,0 +1,3 @@
+docker:
+  image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest
+  digest: sha256:ec49167c606648a063d1222220b48119c912562849a0528f35bfb592a9f72737
diff --git a/.github/.OwlBot.yaml b/.github/.OwlBot.yaml
new file mode 100644
index 0000000..ed6155a
--- /dev/null
+++ b/.github/.OwlBot.yaml
@@ -0,0 +1,18 @@
+# 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.
+
+docker:
+  image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest
+
+begin-after-commit-hash: ee56c3493ec6aeb237ff515ecea949710944a20f
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 0000000..f3c8219
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1,11 @@
+# Code owners file.
+# This file controls who is tagged for review for any given pull request.
+#
+# For syntax help see:
+# https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners#codeowners-syntax
+
+# The @googleapis/yoshi-python is the default owner for changes in this repo
+*               @arithmetic1728 @silvolu @googleapis/yoshi-python
+
+# The python-samples-reviewers team is the default owner for samples changes
+/samples/  @googleapis/python-samples-owners
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
new file mode 100644
index 0000000..939e534
--- /dev/null
+++ b/.github/CONTRIBUTING.md
@@ -0,0 +1,28 @@
+# How to Contribute
+
+We'd love to accept your patches and contributions to this project. There are
+just a few small guidelines you need to follow.
+
+## Contributor License Agreement
+
+Contributions to this project must be accompanied by a Contributor License
+Agreement. You (or your employer) retain the copyright to your contribution;
+this simply gives us permission to use and redistribute your contributions as
+part of the project. Head over to <https://cla.developers.google.com/> to see
+your current agreements on file or to sign a new one.
+
+You generally only need to submit a CLA once, so if you've already submitted one
+(even if it was for a different project), you probably don't need to do it
+again.
+
+## Code reviews
+
+All submissions, including submissions by project members, require review. We
+use GitHub pull requests for this purpose. Consult
+[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
+information on using pull requests.
+
+## Community Guidelines
+
+This project follows [Google's Open Source Community
+Guidelines](https://opensource.google.com/conduct/).
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..e43ad68
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,31 @@
+---
+name: Bug report
+about: Create a report to help us improve
+
+---
+
+Thanks for stopping by to let us know something could be better!
+
+**PLEASE READ**: If you have a support contract with Google, please create an issue in the [support console](https://cloud.google.com/support/) instead of filing on GitHub. This will ensure a timely response.
+
+Please run down the following list and make sure you've tried the usual "quick fixes":
+
+  - Search the issues already opened: https://github.com/googleapis/google-auth-library-python/issues
+
+If you are still having issues, please be sure to include as much information as possible:
+
+#### Environment details
+
+  - OS:
+  - Python version:
+  - pip version:
+  - `google-auth` version:
+
+#### Steps to reproduce
+
+  1. ?
+  2. ?
+
+Making sure to follow these steps will guarantee the quickest resolution possible.
+
+Thanks!
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..6365857
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,18 @@
+---
+name: Feature request
+about: Suggest an idea for this library
+
+---
+
+Thanks for stopping by to let us know something could be better!
+
+**PLEASE READ**: If you have a support contract with Google, please create an issue in the [support console](https://cloud.google.com/support/) instead of filing on GitHub. This will ensure a timely response.
+
+ **Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+ **Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+ **Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+ **Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.github/ISSUE_TEMPLATE/support_request.md b/.github/ISSUE_TEMPLATE/support_request.md
new file mode 100644
index 0000000..9958690
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/support_request.md
@@ -0,0 +1,7 @@
+---
+name: Support request
+about: If you have a support contract with Google, please create an issue in the Google Cloud Support console.
+
+---
+
+**PLEASE READ**: If you have a support contract with Google, please create an issue in the [support console](https://cloud.google.com/support/) instead of filing on GitHub. This will ensure a timely response.
diff --git a/.github/release-please.yml b/.github/release-please.yml
new file mode 100644
index 0000000..4507ad0
--- /dev/null
+++ b/.github/release-please.yml
@@ -0,0 +1 @@
+releaseType: python
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ca0c074
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,48 @@
+# Build artifacts
+*.py[cod]
+__pycache__
+*.egg-info/
+build/
+dist/
+
+# Documentation-related
+docs/_build
+
+# Test files
+.nox/
+.tox/
+.cache/
+.pytest_cache/
+cert_path
+key_path
+
+# Django test database
+db.sqlite3
+
+# Coverage files
+.coverage
+coverage.xml
+*sponge_log.xml
+nosetests.xml
+htmlcov/
+
+# Files with private / local data
+scripts/local_test_setup
+tests/data/key.json
+tests/data/key.p12
+tests/data/user-key.json
+system_tests/data/
+
+# PyCharm configuration:
+.idea
+venv/
+
+# Generated files
+pylintrc
+pylintrc.test
+pytype_output/
+
+.python-version
+.DS_Store
+cert_path
+key_path
\ No newline at end of file
diff --git a/.kokoro/build-systests.sh b/.kokoro/build-systests.sh
new file mode 100755
index 0000000..a2947c2
--- /dev/null
+++ b/.kokoro/build-systests.sh
@@ -0,0 +1,48 @@
+#!/bin/bash
+# Copyright 2018 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
+#
+#     https://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.
+
+set -eo pipefail
+
+if [[ -z "${PROJECT_ROOT:-}" ]]; then
+    PROJECT_ROOT="github/google-auth-library-python"
+fi
+
+cd "${PROJECT_ROOT}"
+
+# Disable buffering, so that the logs stream through.
+export PYTHONUNBUFFERED=1
+
+# Remove old nox
+python3 -m pip uninstall --yes --quiet nox-automation
+
+# Install nox
+python3 -m pip install --upgrade --quiet nox
+python3 -m nox --version
+
+# Setup service account credentials.
+export GOOGLE_APPLICATION_CREDENTIALS=${KOKORO_GFILE_DIR}/service-account.json
+
+# Setup project id.
+export PROJECT_ID=$(cat "${KOKORO_GFILE_DIR}/project-id.txt")
+
+# Activate gcloud with service account credentials
+gcloud auth activate-service-account --key-file=$GOOGLE_APPLICATION_CREDENTIALS
+gcloud config set project ${PROJECT_ID}
+
+# Decrypt system test secrets
+./scripts/decrypt-secrets.sh
+
+# Run system tests which use a different noxfile
+python3 -m nox -f system_tests/noxfile.py
diff --git a/.kokoro/build.sh b/.kokoro/build.sh
new file mode 100755
index 0000000..04ab45c
--- /dev/null
+++ b/.kokoro/build.sh
@@ -0,0 +1,40 @@
+#!/bin/bash
+# Copyright 2018 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
+#
+#     https://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.
+
+set -eo pipefail
+
+if [[ -z "${PROJECT_ROOT:-}" ]]; then
+    PROJECT_ROOT="github/google-auth-library-python"
+fi
+
+cd "${PROJECT_ROOT}"
+
+# Disable buffering, so that the logs stream through.
+export PYTHONUNBUFFERED=1
+
+# Remove old nox
+python3 -m pip uninstall --yes --quiet nox-automation
+
+# Install nox
+python3 -m pip install --upgrade --quiet nox
+python3 -m nox --version
+
+# If NOX_SESSION is set, it only runs the specified session,
+# otherwise run all the sessions.
+if [[ -n "${NOX_SESSION:-}" ]]; then
+    python3 -m nox -s ${NOX_SESSION:-}
+else
+    python3 -m nox
+fi
diff --git a/.kokoro/common.cfg b/.kokoro/common.cfg
new file mode 100644
index 0000000..81f431a
--- /dev/null
+++ b/.kokoro/common.cfg
@@ -0,0 +1,16 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+# Download trampoline resources. These will be in ${KOKORO_GFILE_DIR}
+gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline"
+
+# Download resources for tests
+gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/google-auth-library-python"
+
+# All builds use the trampoline script to run in docker.
+build_file: "google-auth-library-python/.kokoro/trampoline.sh"
+
+# Use the Python worker docker iamge.
+env_vars: {
+    key: "TRAMPOLINE_IMAGE"
+    value: "gcr.io/cloud-devrel-public-resources/python-multi"
+}
diff --git a/.kokoro/continuous/common.cfg b/.kokoro/continuous/common.cfg
new file mode 100644
index 0000000..10910e3
--- /dev/null
+++ b/.kokoro/continuous/common.cfg
@@ -0,0 +1,27 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+# Build logs will be here
+action {
+  define_artifacts {
+    regex: "**/*sponge_log.xml"
+  }
+}
+
+# Download trampoline resources.
+gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline"
+
+# Download resources for system tests (service account key, etc.)
+gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/google-auth-library-python"
+
+# Use the trampoline script to run in docker.
+build_file: "google-auth-library-python/.kokoro/trampoline.sh"
+
+# Configure the docker image for kokoro-trampoline.
+env_vars: {
+    key: "TRAMPOLINE_IMAGE"
+    value: "gcr.io/cloud-devrel-kokoro-resources/python-multi"
+}
+env_vars: {
+    key: "TRAMPOLINE_BUILD_FILE"
+    value: "github/google-auth-library-python/.kokoro/build.sh"
+}
diff --git a/.kokoro/continuous/continuous.cfg b/.kokoro/continuous/continuous.cfg
new file mode 100644
index 0000000..8f43917
--- /dev/null
+++ b/.kokoro/continuous/continuous.cfg
@@ -0,0 +1 @@
+# Format: //devtools/kokoro/config/proto/build.proto
\ No newline at end of file
diff --git a/.kokoro/docker/docs/Dockerfile b/.kokoro/docker/docs/Dockerfile
new file mode 100644
index 0000000..4e1b1fb
--- /dev/null
+++ b/.kokoro/docker/docs/Dockerfile
@@ -0,0 +1,67 @@
+# 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.
+
+from ubuntu:20.04
+
+ENV DEBIAN_FRONTEND noninteractive
+
+# Ensure local Python is preferred over distribution Python.
+ENV PATH /usr/local/bin:$PATH
+
+# Install dependencies.
+RUN apt-get update \
+  && apt-get install -y --no-install-recommends \
+    apt-transport-https \
+    build-essential \
+    ca-certificates \
+    curl \
+    dirmngr \
+    git \
+    gpg-agent \
+    graphviz \
+    libbz2-dev \
+    libdb5.3-dev \
+    libexpat1-dev \
+    libffi-dev \
+    liblzma-dev \
+    libreadline-dev \
+    libsnappy-dev \
+    libssl-dev \
+    libsqlite3-dev \
+    portaudio19-dev \
+    python3-distutils \
+    redis-server \
+    software-properties-common \
+    ssh \
+    sudo \
+    tcl \
+    tcl-dev \
+    tk \
+    tk-dev \
+    uuid-dev \
+    wget \
+    zlib1g-dev \
+  && add-apt-repository universe \
+  && apt-get update \
+  && apt-get -y install jq \
+  && apt-get clean autoclean \
+  && apt-get autoremove -y \
+  && rm -rf /var/lib/apt/lists/* \
+  && rm -f /var/cache/apt/archives/*.deb
+
+RUN wget -O /tmp/get-pip.py 'https://bootstrap.pypa.io/get-pip.py' \
+  && python3.8 /tmp/get-pip.py \
+  && rm /tmp/get-pip.py
+
+CMD ["python3.8"]
diff --git a/.kokoro/docs/common.cfg b/.kokoro/docs/common.cfg
new file mode 100644
index 0000000..980bff5
--- /dev/null
+++ b/.kokoro/docs/common.cfg
@@ -0,0 +1,67 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+# Build logs will be here
+action {
+  define_artifacts {
+    regex: "**/*sponge_log.xml"
+  }
+}
+
+# Download trampoline resources.
+gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline"
+
+# Use the trampoline script to run in docker.
+build_file: "google-auth-library-python/.kokoro/trampoline_v2.sh"
+
+# Configure the docker image for kokoro-trampoline.
+env_vars: {
+    key: "TRAMPOLINE_IMAGE"
+    value: "gcr.io/cloud-devrel-kokoro-resources/python-lib-docs"
+}
+env_vars: {
+    key: "TRAMPOLINE_BUILD_FILE"
+    value: "github/google-auth-library-python/.kokoro/publish-docs.sh"
+}
+
+env_vars: {
+    key: "STAGING_BUCKET"
+    value: "docs-staging"
+}
+
+env_vars: {
+    key: "V2_STAGING_BUCKET"
+    # Push non-cloud library docs to `docs-staging-v2-staging` instead of the
+    # Cloud RAD bucket `docs-staging-v2`
+    value: "docs-staging-v2-staging"
+}
+
+# It will upload the docker image after successful builds.
+env_vars: {
+    key: "TRAMPOLINE_IMAGE_UPLOAD"
+    value: "true"
+}
+
+# It will always build the docker image.
+env_vars: {
+    key: "TRAMPOLINE_DOCKERFILE"
+    value: ".kokoro/docker/docs/Dockerfile"
+}
+
+# Fetch the token needed for reporting release status to GitHub
+before_action {
+  fetch_keystore {
+    keystore_resource {
+      keystore_config_id: 73713
+      keyname: "yoshi-automation-github-key"
+    }
+  }
+}
+
+before_action {
+  fetch_keystore {
+    keystore_resource {
+      keystore_config_id: 73713
+      keyname: "docuploader_service_account"
+    }
+  }
+}
\ No newline at end of file
diff --git a/.kokoro/docs/docs-presubmit.cfg b/.kokoro/docs/docs-presubmit.cfg
new file mode 100644
index 0000000..d3f0dea
--- /dev/null
+++ b/.kokoro/docs/docs-presubmit.cfg
@@ -0,0 +1,28 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+env_vars: {
+    key: "STAGING_BUCKET"
+    value: "gcloud-python-test"
+}
+
+env_vars: {
+    key: "V2_STAGING_BUCKET"
+    value: "gcloud-python-test"
+}
+
+# We only upload the image in the main `docs` build.
+env_vars: {
+    key: "TRAMPOLINE_IMAGE_UPLOAD"
+    value: "false"
+}
+
+env_vars: {
+    key: "TRAMPOLINE_BUILD_FILE"
+    value: "github/google-auth-library-python/.kokoro/build.sh"
+}
+
+# Only run this nox session.
+env_vars: {
+    key: "NOX_SESSION"
+    value: "docs"
+}
diff --git a/.kokoro/docs/docs.cfg b/.kokoro/docs/docs.cfg
new file mode 100644
index 0000000..8f43917
--- /dev/null
+++ b/.kokoro/docs/docs.cfg
@@ -0,0 +1 @@
+# Format: //devtools/kokoro/config/proto/build.proto
\ No newline at end of file
diff --git a/.kokoro/populate-secrets.sh b/.kokoro/populate-secrets.sh
new file mode 100755
index 0000000..f525142
--- /dev/null
+++ b/.kokoro/populate-secrets.sh
@@ -0,0 +1,43 @@
+#!/bin/bash
+# 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.
+
+set -eo pipefail
+
+function now { date +"%Y-%m-%d %H:%M:%S" | tr -d '\n' ;}
+function msg { println "$*" >&2 ;}
+function println { printf '%s\n' "$(now) $*" ;}
+
+
+# Populates requested secrets set in SECRET_MANAGER_KEYS from service account:
+# kokoro-trampoline@cloud-devrel-kokoro-resources.iam.gserviceaccount.com
+SECRET_LOCATION="${KOKORO_GFILE_DIR}/secret_manager"
+msg "Creating folder on disk for secrets: ${SECRET_LOCATION}"
+mkdir -p ${SECRET_LOCATION}
+for key in $(echo ${SECRET_MANAGER_KEYS} | sed "s/,/ /g")
+do
+  msg "Retrieving secret ${key}"
+  docker run --entrypoint=gcloud \
+    --volume=${KOKORO_GFILE_DIR}:${KOKORO_GFILE_DIR} \
+    gcr.io/google.com/cloudsdktool/cloud-sdk \
+    secrets versions access latest \
+    --project cloud-devrel-kokoro-resources \
+    --secret ${key} > \
+    "${SECRET_LOCATION}/${key}"
+  if [[ $? == 0 ]]; then
+    msg "Secret written to ${SECRET_LOCATION}/${key}"
+  else
+    msg "Error retrieving secret ${key}"
+  fi
+done
diff --git a/.kokoro/presubmit/common.cfg b/.kokoro/presubmit/common.cfg
new file mode 100644
index 0000000..10910e3
--- /dev/null
+++ b/.kokoro/presubmit/common.cfg
@@ -0,0 +1,27 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+# Build logs will be here
+action {
+  define_artifacts {
+    regex: "**/*sponge_log.xml"
+  }
+}
+
+# Download trampoline resources.
+gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline"
+
+# Download resources for system tests (service account key, etc.)
+gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/google-auth-library-python"
+
+# Use the trampoline script to run in docker.
+build_file: "google-auth-library-python/.kokoro/trampoline.sh"
+
+# Configure the docker image for kokoro-trampoline.
+env_vars: {
+    key: "TRAMPOLINE_IMAGE"
+    value: "gcr.io/cloud-devrel-kokoro-resources/python-multi"
+}
+env_vars: {
+    key: "TRAMPOLINE_BUILD_FILE"
+    value: "github/google-auth-library-python/.kokoro/build.sh"
+}
diff --git a/.kokoro/presubmit/presubmit.cfg b/.kokoro/presubmit/presubmit.cfg
new file mode 100644
index 0000000..8f43917
--- /dev/null
+++ b/.kokoro/presubmit/presubmit.cfg
@@ -0,0 +1 @@
+# Format: //devtools/kokoro/config/proto/build.proto
\ No newline at end of file
diff --git a/.kokoro/presubmit/system-3.7.cfg b/.kokoro/presubmit/system-3.7.cfg
new file mode 100644
index 0000000..0393b98
--- /dev/null
+++ b/.kokoro/presubmit/system-3.7.cfg
@@ -0,0 +1,5 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+env_vars: {
+    key: "TRAMPOLINE_BUILD_FILE"
+    value: "github/google-auth-library-python/.kokoro/build-systests.sh"
+}
diff --git a/.kokoro/publish-docs.sh b/.kokoro/publish-docs.sh
new file mode 100755
index 0000000..8acb14e
--- /dev/null
+++ b/.kokoro/publish-docs.sh
@@ -0,0 +1,64 @@
+#!/bin/bash
+# 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
+#
+#     https://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.
+
+set -eo pipefail
+
+# Disable buffering, so that the logs stream through.
+export PYTHONUNBUFFERED=1
+
+export PATH="${HOME}/.local/bin:${PATH}"
+
+# Install nox
+python3 -m pip install --user --upgrade --quiet nox
+python3 -m nox --version
+
+# build docs
+nox -s docs
+
+python3 -m pip install --user gcp-docuploader
+
+# create metadata
+python3 -m docuploader create-metadata \
+  --name=$(jq --raw-output '.name // empty' .repo-metadata.json) \
+  --version=$(python3 setup.py --version) \
+  --language=$(jq --raw-output '.language // empty' .repo-metadata.json) \
+  --distribution-name=$(python3 setup.py --name) \
+  --product-page=$(jq --raw-output '.product_documentation // empty' .repo-metadata.json) \
+  --github-repository=$(jq --raw-output '.repo // empty' .repo-metadata.json) \
+  --issue-tracker=$(jq --raw-output '.issue_tracker // empty' .repo-metadata.json)
+
+cat docs.metadata
+
+# upload docs
+python3 -m docuploader upload docs/_build/html --metadata-file docs.metadata --staging-bucket "${STAGING_BUCKET}"
+
+
+# docfx yaml files
+nox -s docfx
+
+# create metadata.
+python3 -m docuploader create-metadata \
+  --name=$(jq --raw-output '.name // empty' .repo-metadata.json) \
+  --version=$(python3 setup.py --version) \
+  --language=$(jq --raw-output '.language // empty' .repo-metadata.json) \
+  --distribution-name=$(python3 setup.py --name) \
+  --product-page=$(jq --raw-output '.product_documentation // empty' .repo-metadata.json) \
+  --github-repository=$(jq --raw-output '.repo // empty' .repo-metadata.json) \
+  --issue-tracker=$(jq --raw-output '.issue_tracker // empty' .repo-metadata.json)
+
+cat docs.metadata
+
+# upload docs
+python3 -m docuploader upload docs/_build/html/docfx_yaml --metadata-file docs.metadata --destination-prefix docfx --staging-bucket "${V2_STAGING_BUCKET}"
diff --git a/.kokoro/release.sh b/.kokoro/release.sh
new file mode 100755
index 0000000..967bc91
--- /dev/null
+++ b/.kokoro/release.sh
@@ -0,0 +1,32 @@
+#!/bin/bash
+# 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
+#
+#     https://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.
+
+set -eo pipefail
+
+# Start the releasetool reporter
+python3 -m pip install gcp-releasetool
+python3 -m releasetool publish-reporter-script > /tmp/publisher-script; source /tmp/publisher-script
+
+# Ensure that we have the latest versions of Twine, Wheel, and Setuptools.
+python3 -m pip install --upgrade twine wheel setuptools
+
+# Disable buffering, so that the logs stream through.
+export PYTHONUNBUFFERED=1
+
+# Move into the package, build the distribution and upload.
+TWINE_PASSWORD=$(cat "${KOKORO_GFILE_DIR}/secret_manager/google-cloud-pypi-token")
+cd github/google-auth-library-python
+python3 setup.py sdist bdist_wheel
+twine upload --username __token__ --password "${TWINE_PASSWORD}" dist/*
diff --git a/.kokoro/release/common.cfg b/.kokoro/release/common.cfg
new file mode 100644
index 0000000..07334fd
--- /dev/null
+++ b/.kokoro/release/common.cfg
@@ -0,0 +1,30 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+# Build logs will be here
+action {
+  define_artifacts {
+    regex: "**/*sponge_log.xml"
+  }
+}
+
+# Download trampoline resources.
+gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline"
+
+# Use the trampoline script to run in docker.
+build_file: "google-auth-library-python/.kokoro/trampoline.sh"
+
+# Configure the docker image for kokoro-trampoline.
+env_vars: {
+    key: "TRAMPOLINE_IMAGE"
+    value: "gcr.io/cloud-devrel-kokoro-resources/python-multi"
+}
+env_vars: {
+    key: "TRAMPOLINE_BUILD_FILE"
+    value: "github/google-auth-library-python/.kokoro/release.sh"
+}
+
+# Tokens needed to report release status back to GitHub
+env_vars: {
+  key: "SECRET_MANAGER_KEYS"
+  value: "releasetool-publish-reporter-app,releasetool-publish-reporter-googleapis-installation,releasetool-publish-reporter-pem,google-cloud-pypi-token"
+}
diff --git a/.kokoro/release/release.cfg b/.kokoro/release/release.cfg
new file mode 100644
index 0000000..8f43917
--- /dev/null
+++ b/.kokoro/release/release.cfg
@@ -0,0 +1 @@
+# Format: //devtools/kokoro/config/proto/build.proto
\ No newline at end of file
diff --git a/.kokoro/samples/lint/common.cfg b/.kokoro/samples/lint/common.cfg
new file mode 100644
index 0000000..f6b0c07
--- /dev/null
+++ b/.kokoro/samples/lint/common.cfg
@@ -0,0 +1,34 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+# Build logs will be here
+action {
+  define_artifacts {
+    regex: "**/*sponge_log.xml"
+  }
+}
+
+# Specify which tests to run
+env_vars: {
+    key: "RUN_TESTS_SESSION"
+    value: "lint"
+}
+
+env_vars: {
+    key: "TRAMPOLINE_BUILD_FILE"
+    value: "github/google-auth-library-python/.kokoro/test-samples.sh"
+}
+
+# Configure the docker image for kokoro-trampoline.
+env_vars: {
+    key: "TRAMPOLINE_IMAGE"
+    value: "gcr.io/cloud-devrel-kokoro-resources/python-samples-testing-docker"
+}
+
+# Download secrets for samples
+gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/python-docs-samples"
+
+# Download trampoline resources.
+gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline"
+
+# Use the trampoline script to run in docker.
+build_file: "google-auth-library-python/.kokoro/trampoline_v2.sh"
\ No newline at end of file
diff --git a/.kokoro/samples/lint/continuous.cfg b/.kokoro/samples/lint/continuous.cfg
new file mode 100644
index 0000000..a1c8d97
--- /dev/null
+++ b/.kokoro/samples/lint/continuous.cfg
@@ -0,0 +1,6 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+env_vars: {
+    key: "INSTALL_LIBRARY_FROM_SOURCE"
+    value: "True"
+}
\ No newline at end of file
diff --git a/.kokoro/samples/lint/periodic.cfg b/.kokoro/samples/lint/periodic.cfg
new file mode 100644
index 0000000..50fec96
--- /dev/null
+++ b/.kokoro/samples/lint/periodic.cfg
@@ -0,0 +1,6 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+env_vars: {
+    key: "INSTALL_LIBRARY_FROM_SOURCE"
+    value: "False"
+}
\ No newline at end of file
diff --git a/.kokoro/samples/lint/presubmit.cfg b/.kokoro/samples/lint/presubmit.cfg
new file mode 100644
index 0000000..a1c8d97
--- /dev/null
+++ b/.kokoro/samples/lint/presubmit.cfg
@@ -0,0 +1,6 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+env_vars: {
+    key: "INSTALL_LIBRARY_FROM_SOURCE"
+    value: "True"
+}
\ No newline at end of file
diff --git a/.kokoro/samples/python3.10/common.cfg b/.kokoro/samples/python3.10/common.cfg
new file mode 100644
index 0000000..de052d3
--- /dev/null
+++ b/.kokoro/samples/python3.10/common.cfg
@@ -0,0 +1,40 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+# Build logs will be here
+action {
+  define_artifacts {
+    regex: "**/*sponge_log.xml"
+  }
+}
+
+# Specify which tests to run
+env_vars: {
+    key: "RUN_TESTS_SESSION"
+    value: "py-3.10"
+}
+
+# Declare build specific Cloud project.
+env_vars: {
+    key: "BUILD_SPECIFIC_GCLOUD_PROJECT"
+    value: "python-docs-samples-tests-310"
+}
+
+env_vars: {
+    key: "TRAMPOLINE_BUILD_FILE"
+    value: "github/google-auth-library-python/.kokoro/test-samples.sh"
+}
+
+# Configure the docker image for kokoro-trampoline.
+env_vars: {
+    key: "TRAMPOLINE_IMAGE"
+    value: "gcr.io/cloud-devrel-kokoro-resources/python-samples-testing-docker"
+}
+
+# Download secrets for samples
+gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/python-docs-samples"
+
+# Download trampoline resources.
+gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline"
+
+# Use the trampoline script to run in docker.
+build_file: "google-auth-library-python/.kokoro/trampoline_v2.sh"
\ No newline at end of file
diff --git a/.kokoro/samples/python3.10/continuous.cfg b/.kokoro/samples/python3.10/continuous.cfg
new file mode 100644
index 0000000..a1c8d97
--- /dev/null
+++ b/.kokoro/samples/python3.10/continuous.cfg
@@ -0,0 +1,6 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+env_vars: {
+    key: "INSTALL_LIBRARY_FROM_SOURCE"
+    value: "True"
+}
\ No newline at end of file
diff --git a/.kokoro/samples/python3.10/periodic-head.cfg b/.kokoro/samples/python3.10/periodic-head.cfg
new file mode 100644
index 0000000..83eace8
--- /dev/null
+++ b/.kokoro/samples/python3.10/periodic-head.cfg
@@ -0,0 +1,11 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+env_vars: {
+    key: "INSTALL_LIBRARY_FROM_SOURCE"
+    value: "True"
+}
+
+env_vars: {
+    key: "TRAMPOLINE_BUILD_FILE"
+    value: "github/google-auth-library-python/.kokoro/test-samples-against-head.sh"
+}
diff --git a/.kokoro/samples/python3.10/periodic.cfg b/.kokoro/samples/python3.10/periodic.cfg
new file mode 100644
index 0000000..71cd1e5
--- /dev/null
+++ b/.kokoro/samples/python3.10/periodic.cfg
@@ -0,0 +1,6 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+env_vars: {
+    key: "INSTALL_LIBRARY_FROM_SOURCE"
+    value: "False"
+}
diff --git a/.kokoro/samples/python3.10/presubmit.cfg b/.kokoro/samples/python3.10/presubmit.cfg
new file mode 100644
index 0000000..a1c8d97
--- /dev/null
+++ b/.kokoro/samples/python3.10/presubmit.cfg
@@ -0,0 +1,6 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+env_vars: {
+    key: "INSTALL_LIBRARY_FROM_SOURCE"
+    value: "True"
+}
\ No newline at end of file
diff --git a/.kokoro/samples/python3.6/common.cfg b/.kokoro/samples/python3.6/common.cfg
new file mode 100644
index 0000000..57feb84
--- /dev/null
+++ b/.kokoro/samples/python3.6/common.cfg
@@ -0,0 +1,40 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+# Build logs will be here
+action {
+  define_artifacts {
+    regex: "**/*sponge_log.xml"
+  }
+}
+
+# Specify which tests to run
+env_vars: {
+    key: "RUN_TESTS_SESSION"
+    value: "py-3.6"
+}
+
+# Declare build specific Cloud project.
+env_vars: {
+    key: "BUILD_SPECIFIC_GCLOUD_PROJECT"
+    value: "python-docs-samples-tests-py36"
+}
+
+env_vars: {
+    key: "TRAMPOLINE_BUILD_FILE"
+    value: "github/google-auth-library-python/.kokoro/test-samples.sh"
+}
+
+# Configure the docker image for kokoro-trampoline.
+env_vars: {
+    key: "TRAMPOLINE_IMAGE"
+    value: "gcr.io/cloud-devrel-kokoro-resources/python-samples-testing-docker"
+}
+
+# Download secrets for samples
+gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/python-docs-samples"
+
+# Download trampoline resources.
+gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline"
+
+# Use the trampoline script to run in docker.
+build_file: "google-auth-library-python/.kokoro/trampoline_v2.sh"
\ No newline at end of file
diff --git a/.kokoro/samples/python3.6/continuous.cfg b/.kokoro/samples/python3.6/continuous.cfg
new file mode 100644
index 0000000..7218af1
--- /dev/null
+++ b/.kokoro/samples/python3.6/continuous.cfg
@@ -0,0 +1,7 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+env_vars: {
+    key: "INSTALL_LIBRARY_FROM_SOURCE"
+    value: "True"
+}
+
diff --git a/.kokoro/samples/python3.6/periodic-head.cfg b/.kokoro/samples/python3.6/periodic-head.cfg
new file mode 100644
index 0000000..83eace8
--- /dev/null
+++ b/.kokoro/samples/python3.6/periodic-head.cfg
@@ -0,0 +1,11 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+env_vars: {
+    key: "INSTALL_LIBRARY_FROM_SOURCE"
+    value: "True"
+}
+
+env_vars: {
+    key: "TRAMPOLINE_BUILD_FILE"
+    value: "github/google-auth-library-python/.kokoro/test-samples-against-head.sh"
+}
diff --git a/.kokoro/samples/python3.6/periodic.cfg b/.kokoro/samples/python3.6/periodic.cfg
new file mode 100644
index 0000000..71cd1e5
--- /dev/null
+++ b/.kokoro/samples/python3.6/periodic.cfg
@@ -0,0 +1,6 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+env_vars: {
+    key: "INSTALL_LIBRARY_FROM_SOURCE"
+    value: "False"
+}
diff --git a/.kokoro/samples/python3.6/presubmit.cfg b/.kokoro/samples/python3.6/presubmit.cfg
new file mode 100644
index 0000000..a1c8d97
--- /dev/null
+++ b/.kokoro/samples/python3.6/presubmit.cfg
@@ -0,0 +1,6 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+env_vars: {
+    key: "INSTALL_LIBRARY_FROM_SOURCE"
+    value: "True"
+}
\ No newline at end of file
diff --git a/.kokoro/samples/python3.7/common.cfg b/.kokoro/samples/python3.7/common.cfg
new file mode 100644
index 0000000..7ca2eb0
--- /dev/null
+++ b/.kokoro/samples/python3.7/common.cfg
@@ -0,0 +1,40 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+# Build logs will be here
+action {
+  define_artifacts {
+    regex: "**/*sponge_log.xml"
+  }
+}
+
+# Specify which tests to run
+env_vars: {
+    key: "RUN_TESTS_SESSION"
+    value: "py-3.7"
+}
+
+# Declare build specific Cloud project.
+env_vars: {
+    key: "BUILD_SPECIFIC_GCLOUD_PROJECT"
+    value: "python-docs-samples-tests-py37"
+}
+
+env_vars: {
+    key: "TRAMPOLINE_BUILD_FILE"
+    value: "github/google-auth-library-python/.kokoro/test-samples.sh"
+}
+
+# Configure the docker image for kokoro-trampoline.
+env_vars: {
+    key: "TRAMPOLINE_IMAGE"
+    value: "gcr.io/cloud-devrel-kokoro-resources/python-samples-testing-docker"
+}
+
+# Download secrets for samples
+gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/python-docs-samples"
+
+# Download trampoline resources.
+gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline"
+
+# Use the trampoline script to run in docker.
+build_file: "google-auth-library-python/.kokoro/trampoline_v2.sh"
\ No newline at end of file
diff --git a/.kokoro/samples/python3.7/continuous.cfg b/.kokoro/samples/python3.7/continuous.cfg
new file mode 100644
index 0000000..a1c8d97
--- /dev/null
+++ b/.kokoro/samples/python3.7/continuous.cfg
@@ -0,0 +1,6 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+env_vars: {
+    key: "INSTALL_LIBRARY_FROM_SOURCE"
+    value: "True"
+}
\ No newline at end of file
diff --git a/.kokoro/samples/python3.7/periodic-head.cfg b/.kokoro/samples/python3.7/periodic-head.cfg
new file mode 100644
index 0000000..83eace8
--- /dev/null
+++ b/.kokoro/samples/python3.7/periodic-head.cfg
@@ -0,0 +1,11 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+env_vars: {
+    key: "INSTALL_LIBRARY_FROM_SOURCE"
+    value: "True"
+}
+
+env_vars: {
+    key: "TRAMPOLINE_BUILD_FILE"
+    value: "github/google-auth-library-python/.kokoro/test-samples-against-head.sh"
+}
diff --git a/.kokoro/samples/python3.7/periodic.cfg b/.kokoro/samples/python3.7/periodic.cfg
new file mode 100644
index 0000000..71cd1e5
--- /dev/null
+++ b/.kokoro/samples/python3.7/periodic.cfg
@@ -0,0 +1,6 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+env_vars: {
+    key: "INSTALL_LIBRARY_FROM_SOURCE"
+    value: "False"
+}
diff --git a/.kokoro/samples/python3.7/presubmit.cfg b/.kokoro/samples/python3.7/presubmit.cfg
new file mode 100644
index 0000000..a1c8d97
--- /dev/null
+++ b/.kokoro/samples/python3.7/presubmit.cfg
@@ -0,0 +1,6 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+env_vars: {
+    key: "INSTALL_LIBRARY_FROM_SOURCE"
+    value: "True"
+}
\ No newline at end of file
diff --git a/.kokoro/samples/python3.8/common.cfg b/.kokoro/samples/python3.8/common.cfg
new file mode 100644
index 0000000..fbd029e
--- /dev/null
+++ b/.kokoro/samples/python3.8/common.cfg
@@ -0,0 +1,40 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+# Build logs will be here
+action {
+  define_artifacts {
+    regex: "**/*sponge_log.xml"
+  }
+}
+
+# Specify which tests to run
+env_vars: {
+    key: "RUN_TESTS_SESSION"
+    value: "py-3.8"
+}
+
+# Declare build specific Cloud project.
+env_vars: {
+    key: "BUILD_SPECIFIC_GCLOUD_PROJECT"
+    value: "python-docs-samples-tests-py38"
+}
+
+env_vars: {
+    key: "TRAMPOLINE_BUILD_FILE"
+    value: "github/google-auth-library-python/.kokoro/test-samples.sh"
+}
+
+# Configure the docker image for kokoro-trampoline.
+env_vars: {
+    key: "TRAMPOLINE_IMAGE"
+    value: "gcr.io/cloud-devrel-kokoro-resources/python-samples-testing-docker"
+}
+
+# Download secrets for samples
+gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/python-docs-samples"
+
+# Download trampoline resources.
+gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline"
+
+# Use the trampoline script to run in docker.
+build_file: "google-auth-library-python/.kokoro/trampoline_v2.sh"
\ No newline at end of file
diff --git a/.kokoro/samples/python3.8/continuous.cfg b/.kokoro/samples/python3.8/continuous.cfg
new file mode 100644
index 0000000..a1c8d97
--- /dev/null
+++ b/.kokoro/samples/python3.8/continuous.cfg
@@ -0,0 +1,6 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+env_vars: {
+    key: "INSTALL_LIBRARY_FROM_SOURCE"
+    value: "True"
+}
\ No newline at end of file
diff --git a/.kokoro/samples/python3.8/periodic-head.cfg b/.kokoro/samples/python3.8/periodic-head.cfg
new file mode 100644
index 0000000..83eace8
--- /dev/null
+++ b/.kokoro/samples/python3.8/periodic-head.cfg
@@ -0,0 +1,11 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+env_vars: {
+    key: "INSTALL_LIBRARY_FROM_SOURCE"
+    value: "True"
+}
+
+env_vars: {
+    key: "TRAMPOLINE_BUILD_FILE"
+    value: "github/google-auth-library-python/.kokoro/test-samples-against-head.sh"
+}
diff --git a/.kokoro/samples/python3.8/periodic.cfg b/.kokoro/samples/python3.8/periodic.cfg
new file mode 100644
index 0000000..71cd1e5
--- /dev/null
+++ b/.kokoro/samples/python3.8/periodic.cfg
@@ -0,0 +1,6 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+env_vars: {
+    key: "INSTALL_LIBRARY_FROM_SOURCE"
+    value: "False"
+}
diff --git a/.kokoro/samples/python3.8/presubmit.cfg b/.kokoro/samples/python3.8/presubmit.cfg
new file mode 100644
index 0000000..a1c8d97
--- /dev/null
+++ b/.kokoro/samples/python3.8/presubmit.cfg
@@ -0,0 +1,6 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+env_vars: {
+    key: "INSTALL_LIBRARY_FROM_SOURCE"
+    value: "True"
+}
\ No newline at end of file
diff --git a/.kokoro/samples/python3.9/common.cfg b/.kokoro/samples/python3.9/common.cfg
new file mode 100644
index 0000000..07cda0a
--- /dev/null
+++ b/.kokoro/samples/python3.9/common.cfg
@@ -0,0 +1,40 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+# Build logs will be here
+action {
+  define_artifacts {
+    regex: "**/*sponge_log.xml"
+  }
+}
+
+# Specify which tests to run
+env_vars: {
+    key: "RUN_TESTS_SESSION"
+    value: "py-3.9"
+}
+
+# Declare build specific Cloud project.
+env_vars: {
+    key: "BUILD_SPECIFIC_GCLOUD_PROJECT"
+    value: "python-docs-samples-tests-py39"
+}
+
+env_vars: {
+    key: "TRAMPOLINE_BUILD_FILE"
+    value: "github/google-auth-library-python/.kokoro/test-samples.sh"
+}
+
+# Configure the docker image for kokoro-trampoline.
+env_vars: {
+    key: "TRAMPOLINE_IMAGE"
+    value: "gcr.io/cloud-devrel-kokoro-resources/python-samples-testing-docker"
+}
+
+# Download secrets for samples
+gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/python-docs-samples"
+
+# Download trampoline resources.
+gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline"
+
+# Use the trampoline script to run in docker.
+build_file: "google-auth-library-python/.kokoro/trampoline_v2.sh"
\ No newline at end of file
diff --git a/.kokoro/samples/python3.9/continuous.cfg b/.kokoro/samples/python3.9/continuous.cfg
new file mode 100644
index 0000000..a1c8d97
--- /dev/null
+++ b/.kokoro/samples/python3.9/continuous.cfg
@@ -0,0 +1,6 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+env_vars: {
+    key: "INSTALL_LIBRARY_FROM_SOURCE"
+    value: "True"
+}
\ No newline at end of file
diff --git a/.kokoro/samples/python3.9/periodic-head.cfg b/.kokoro/samples/python3.9/periodic-head.cfg
new file mode 100644
index 0000000..83eace8
--- /dev/null
+++ b/.kokoro/samples/python3.9/periodic-head.cfg
@@ -0,0 +1,11 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+env_vars: {
+    key: "INSTALL_LIBRARY_FROM_SOURCE"
+    value: "True"
+}
+
+env_vars: {
+    key: "TRAMPOLINE_BUILD_FILE"
+    value: "github/google-auth-library-python/.kokoro/test-samples-against-head.sh"
+}
diff --git a/.kokoro/samples/python3.9/periodic.cfg b/.kokoro/samples/python3.9/periodic.cfg
new file mode 100644
index 0000000..71cd1e5
--- /dev/null
+++ b/.kokoro/samples/python3.9/periodic.cfg
@@ -0,0 +1,6 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+env_vars: {
+    key: "INSTALL_LIBRARY_FROM_SOURCE"
+    value: "False"
+}
diff --git a/.kokoro/samples/python3.9/presubmit.cfg b/.kokoro/samples/python3.9/presubmit.cfg
new file mode 100644
index 0000000..a1c8d97
--- /dev/null
+++ b/.kokoro/samples/python3.9/presubmit.cfg
@@ -0,0 +1,6 @@
+# Format: //devtools/kokoro/config/proto/build.proto
+
+env_vars: {
+    key: "INSTALL_LIBRARY_FROM_SOURCE"
+    value: "True"
+}
\ No newline at end of file
diff --git a/.kokoro/test-samples-against-head.sh b/.kokoro/test-samples-against-head.sh
new file mode 100755
index 0000000..ba3a707
--- /dev/null
+++ b/.kokoro/test-samples-against-head.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+# 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
+#
+#     https://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.
+
+# A customized test runner for samples.
+#
+# For periodic builds, you can specify this file for testing against head.
+
+# `-e` enables the script to automatically fail when a command fails
+# `-o pipefail` sets the exit code to the rightmost comment to exit with a non-zero
+set -eo pipefail
+# Enables `**` to include files nested inside sub-folders
+shopt -s globstar
+
+exec .kokoro/test-samples-impl.sh
diff --git a/.kokoro/test-samples-impl.sh b/.kokoro/test-samples-impl.sh
new file mode 100755
index 0000000..8a324c9
--- /dev/null
+++ b/.kokoro/test-samples-impl.sh
@@ -0,0 +1,102 @@
+#!/bin/bash
+# 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
+#
+#     https://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.
+
+
+# `-e` enables the script to automatically fail when a command fails
+# `-o pipefail` sets the exit code to the rightmost comment to exit with a non-zero
+set -eo pipefail
+# Enables `**` to include files nested inside sub-folders
+shopt -s globstar
+
+# Exit early if samples don't exist
+if ! find samples -name 'requirements.txt' | grep -q .; then
+  echo "No tests run. './samples/**/requirements.txt' not found"
+  exit 0
+fi
+
+# Disable buffering, so that the logs stream through.
+export PYTHONUNBUFFERED=1
+
+# Debug: show build environment
+env | grep KOKORO
+
+# Install nox
+python3.6 -m pip install --upgrade --quiet nox
+
+# Use secrets acessor service account to get secrets
+if [[ -f "${KOKORO_GFILE_DIR}/secrets_viewer_service_account.json" ]]; then
+    gcloud auth activate-service-account \
+	   --key-file="${KOKORO_GFILE_DIR}/secrets_viewer_service_account.json" \
+	   --project="cloud-devrel-kokoro-resources"
+fi
+
+# This script will create 3 files:
+# - testing/test-env.sh
+# - testing/service-account.json
+# - testing/client-secrets.json
+./scripts/decrypt-secrets.sh
+
+source ./testing/test-env.sh
+export GOOGLE_APPLICATION_CREDENTIALS=$(pwd)/testing/service-account.json
+
+# For cloud-run session, we activate the service account for gcloud sdk.
+gcloud auth activate-service-account \
+       --key-file "${GOOGLE_APPLICATION_CREDENTIALS}"
+
+export GOOGLE_CLIENT_SECRETS=$(pwd)/testing/client-secrets.json
+
+echo -e "\n******************** TESTING PROJECTS ********************"
+
+# Switch to 'fail at end' to allow all tests to complete before exiting.
+set +e
+# Use RTN to return a non-zero value if the test fails.
+RTN=0
+ROOT=$(pwd)
+# Find all requirements.txt in the samples directory (may break on whitespace).
+for file in samples/**/requirements.txt; do
+    cd "$ROOT"
+    # Navigate to the project folder.
+    file=$(dirname "$file")
+    cd "$file"
+
+    echo "------------------------------------------------------------"
+    echo "- testing $file"
+    echo "------------------------------------------------------------"
+
+    # Use nox to execute the tests for the project.
+    python3.6 -m nox -s "$RUN_TESTS_SESSION"
+    EXIT=$?
+
+    # If this is a periodic build, send the test log to the FlakyBot.
+    # See https://github.com/googleapis/repo-automation-bots/tree/main/packages/flakybot.
+    if [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"periodic"* ]]; then
+      chmod +x $KOKORO_GFILE_DIR/linux_amd64/flakybot
+      $KOKORO_GFILE_DIR/linux_amd64/flakybot
+    fi
+
+    if [[ $EXIT -ne 0 ]]; then
+      RTN=1
+      echo -e "\n Testing failed: Nox returned a non-zero exit code. \n"
+    else
+      echo -e "\n Testing completed.\n"
+    fi
+
+done
+cd "$ROOT"
+
+# Workaround for Kokoro permissions issue: delete secrets
+rm testing/{test-env.sh,client-secrets.json,service-account.json}
+
+exit "$RTN"
diff --git a/.kokoro/test-samples.sh b/.kokoro/test-samples.sh
new file mode 100755
index 0000000..11c042d
--- /dev/null
+++ b/.kokoro/test-samples.sh
@@ -0,0 +1,44 @@
+#!/bin/bash
+# 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
+#
+#     https://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.
+
+# The default test runner for samples.
+#
+# For periodic builds, we rewinds the repo to the latest release, and
+# run test-samples-impl.sh.
+
+# `-e` enables the script to automatically fail when a command fails
+# `-o pipefail` sets the exit code to the rightmost comment to exit with a non-zero
+set -eo pipefail
+# Enables `**` to include files nested inside sub-folders
+shopt -s globstar
+
+# Run periodic samples tests at latest release
+if [[ $KOKORO_BUILD_ARTIFACTS_SUBDIR = *"periodic"* ]]; then
+    # preserving the test runner implementation.
+    cp .kokoro/test-samples-impl.sh "${TMPDIR}/test-samples-impl.sh"
+    echo "--- IMPORTANT IMPORTANT IMPORTANT ---"
+    echo "Now we rewind the repo back to the latest release..."
+    LATEST_RELEASE=$(git describe --abbrev=0 --tags)
+    git checkout $LATEST_RELEASE
+    echo "The current head is: "
+    echo $(git rev-parse --verify HEAD)
+    echo "--- IMPORTANT IMPORTANT IMPORTANT ---"
+    # move back the test runner implementation if there's no file.
+    if [ ! -f .kokoro/test-samples-impl.sh ]; then
+	cp "${TMPDIR}/test-samples-impl.sh" .kokoro/test-samples-impl.sh
+    fi
+fi
+
+exec .kokoro/test-samples-impl.sh
diff --git a/.kokoro/trampoline.sh b/.kokoro/trampoline.sh
new file mode 100755
index 0000000..f39236e
--- /dev/null
+++ b/.kokoro/trampoline.sh
@@ -0,0 +1,28 @@
+#!/bin/bash
+# Copyright 2017 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set -eo pipefail
+
+# Always run the cleanup script, regardless of the success of bouncing into
+# the container.
+function cleanup() {
+    chmod +x ${KOKORO_GFILE_DIR}/trampoline_cleanup.sh
+    ${KOKORO_GFILE_DIR}/trampoline_cleanup.sh
+    echo "cleanup";
+}
+trap cleanup EXIT
+
+$(dirname $0)/populate-secrets.sh # Secret Manager secrets.
+python3 "${KOKORO_GFILE_DIR}/trampoline_v1.py"
\ No newline at end of file
diff --git a/.kokoro/trampoline_v2.sh b/.kokoro/trampoline_v2.sh
new file mode 100755
index 0000000..4af6cdc
--- /dev/null
+++ b/.kokoro/trampoline_v2.sh
@@ -0,0 +1,487 @@
+#!/usr/bin/env bash
+# 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.
+
+# trampoline_v2.sh
+#
+# This script does 3 things.
+#
+# 1. Prepare the Docker image for the test
+# 2. Run the Docker with appropriate flags to run the test
+# 3. Upload the newly built Docker image
+#
+# in a way that is somewhat compatible with trampoline_v1.
+#
+# To run this script, first download few files from gcs to /dev/shm.
+# (/dev/shm is passed into the container as KOKORO_GFILE_DIR).
+#
+# gsutil cp gs://cloud-devrel-kokoro-resources/python-docs-samples/secrets_viewer_service_account.json /dev/shm
+# gsutil cp gs://cloud-devrel-kokoro-resources/python-docs-samples/automl_secrets.txt /dev/shm
+#
+# Then run the script.
+# .kokoro/trampoline_v2.sh
+#
+# These environment variables are required:
+# TRAMPOLINE_IMAGE: The docker image to use.
+# TRAMPOLINE_DOCKERFILE: The location of the Dockerfile.
+#
+# You can optionally change these environment variables:
+# TRAMPOLINE_IMAGE_UPLOAD:
+#     (true|false): Whether to upload the Docker image after the
+#                   successful builds.
+# TRAMPOLINE_BUILD_FILE: The script to run in the docker container.
+# TRAMPOLINE_WORKSPACE: The workspace path in the docker container.
+#                       Defaults to /workspace.
+# Potentially there are some repo specific envvars in .trampolinerc in
+# the project root.
+
+
+set -euo pipefail
+
+TRAMPOLINE_VERSION="2.0.5"
+
+if command -v tput >/dev/null && [[ -n "${TERM:-}" ]]; then
+  readonly IO_COLOR_RED="$(tput setaf 1)"
+  readonly IO_COLOR_GREEN="$(tput setaf 2)"
+  readonly IO_COLOR_YELLOW="$(tput setaf 3)"
+  readonly IO_COLOR_RESET="$(tput sgr0)"
+else
+  readonly IO_COLOR_RED=""
+  readonly IO_COLOR_GREEN=""
+  readonly IO_COLOR_YELLOW=""
+  readonly IO_COLOR_RESET=""
+fi
+
+function function_exists {
+    [ $(LC_ALL=C type -t $1)"" == "function" ]
+}
+
+# Logs a message using the given color. The first argument must be one
+# of the IO_COLOR_* variables defined above, such as
+# "${IO_COLOR_YELLOW}". The remaining arguments will be logged in the
+# given color. The log message will also have an RFC-3339 timestamp
+# prepended (in UTC). You can disable the color output by setting
+# TERM=vt100.
+function log_impl() {
+    local color="$1"
+    shift
+    local timestamp="$(date -u "+%Y-%m-%dT%H:%M:%SZ")"
+    echo "================================================================"
+    echo "${color}${timestamp}:" "$@" "${IO_COLOR_RESET}"
+    echo "================================================================"
+}
+
+# Logs the given message with normal coloring and a timestamp.
+function log() {
+  log_impl "${IO_COLOR_RESET}" "$@"
+}
+
+# Logs the given message in green with a timestamp.
+function log_green() {
+  log_impl "${IO_COLOR_GREEN}" "$@"
+}
+
+# Logs the given message in yellow with a timestamp.
+function log_yellow() {
+  log_impl "${IO_COLOR_YELLOW}" "$@"
+}
+
+# Logs the given message in red with a timestamp.
+function log_red() {
+  log_impl "${IO_COLOR_RED}" "$@"
+}
+
+readonly tmpdir=$(mktemp -d -t ci-XXXXXXXX)
+readonly tmphome="${tmpdir}/h"
+mkdir -p "${tmphome}"
+
+function cleanup() {
+    rm -rf "${tmpdir}"
+}
+trap cleanup EXIT
+
+RUNNING_IN_CI="${RUNNING_IN_CI:-false}"
+
+# The workspace in the container, defaults to /workspace.
+TRAMPOLINE_WORKSPACE="${TRAMPOLINE_WORKSPACE:-/workspace}"
+
+pass_down_envvars=(
+    # TRAMPOLINE_V2 variables.
+    # Tells scripts whether they are running as part of CI or not.
+    "RUNNING_IN_CI"
+    # Indicates which CI system we're in.
+    "TRAMPOLINE_CI"
+    # Indicates the version of the script.
+    "TRAMPOLINE_VERSION"
+)
+
+log_yellow "Building with Trampoline ${TRAMPOLINE_VERSION}"
+
+# Detect which CI systems we're in. If we're in any of the CI systems
+# we support, `RUNNING_IN_CI` will be true and `TRAMPOLINE_CI` will be
+# the name of the CI system. Both envvars will be passing down to the
+# container for telling which CI system we're in.
+if [[ -n "${KOKORO_BUILD_ID:-}" ]]; then
+    # descriptive env var for indicating it's on CI.
+    RUNNING_IN_CI="true"
+    TRAMPOLINE_CI="kokoro"
+    if [[ "${TRAMPOLINE_USE_LEGACY_SERVICE_ACCOUNT:-}" == "true" ]]; then
+	if [[ ! -f "${KOKORO_GFILE_DIR}/kokoro-trampoline.service-account.json" ]]; then
+	    log_red "${KOKORO_GFILE_DIR}/kokoro-trampoline.service-account.json does not exist. Did you forget to mount cloud-devrel-kokoro-resources/trampoline? Aborting."
+	    exit 1
+	fi
+	# This service account will be activated later.
+	TRAMPOLINE_SERVICE_ACCOUNT="${KOKORO_GFILE_DIR}/kokoro-trampoline.service-account.json"
+    else
+	if [[ "${TRAMPOLINE_VERBOSE:-}" == "true" ]]; then
+	    gcloud auth list
+	fi
+	log_yellow "Configuring Container Registry access"
+	gcloud auth configure-docker --quiet
+    fi
+    pass_down_envvars+=(
+	# KOKORO dynamic variables.
+	"KOKORO_BUILD_NUMBER"
+	"KOKORO_BUILD_ID"
+	"KOKORO_JOB_NAME"
+	"KOKORO_GIT_COMMIT"
+	"KOKORO_GITHUB_COMMIT"
+	"KOKORO_GITHUB_PULL_REQUEST_NUMBER"
+	"KOKORO_GITHUB_PULL_REQUEST_COMMIT"
+	# For FlakyBot
+	"KOKORO_GITHUB_COMMIT_URL"
+	"KOKORO_GITHUB_PULL_REQUEST_URL"
+    )
+elif [[ "${TRAVIS:-}" == "true" ]]; then
+    RUNNING_IN_CI="true"
+    TRAMPOLINE_CI="travis"
+    pass_down_envvars+=(
+	"TRAVIS_BRANCH"
+	"TRAVIS_BUILD_ID"
+	"TRAVIS_BUILD_NUMBER"
+	"TRAVIS_BUILD_WEB_URL"
+	"TRAVIS_COMMIT"
+	"TRAVIS_COMMIT_MESSAGE"
+	"TRAVIS_COMMIT_RANGE"
+	"TRAVIS_JOB_NAME"
+	"TRAVIS_JOB_NUMBER"
+	"TRAVIS_JOB_WEB_URL"
+	"TRAVIS_PULL_REQUEST"
+	"TRAVIS_PULL_REQUEST_BRANCH"
+	"TRAVIS_PULL_REQUEST_SHA"
+	"TRAVIS_PULL_REQUEST_SLUG"
+	"TRAVIS_REPO_SLUG"
+	"TRAVIS_SECURE_ENV_VARS"
+	"TRAVIS_TAG"
+    )
+elif [[ -n "${GITHUB_RUN_ID:-}" ]]; then
+    RUNNING_IN_CI="true"
+    TRAMPOLINE_CI="github-workflow"
+    pass_down_envvars+=(
+	"GITHUB_WORKFLOW"
+	"GITHUB_RUN_ID"
+	"GITHUB_RUN_NUMBER"
+	"GITHUB_ACTION"
+	"GITHUB_ACTIONS"
+	"GITHUB_ACTOR"
+	"GITHUB_REPOSITORY"
+	"GITHUB_EVENT_NAME"
+	"GITHUB_EVENT_PATH"
+	"GITHUB_SHA"
+	"GITHUB_REF"
+	"GITHUB_HEAD_REF"
+	"GITHUB_BASE_REF"
+    )
+elif [[ "${CIRCLECI:-}" == "true" ]]; then
+    RUNNING_IN_CI="true"
+    TRAMPOLINE_CI="circleci"
+    pass_down_envvars+=(
+	"CIRCLE_BRANCH"
+	"CIRCLE_BUILD_NUM"
+	"CIRCLE_BUILD_URL"
+	"CIRCLE_COMPARE_URL"
+	"CIRCLE_JOB"
+	"CIRCLE_NODE_INDEX"
+	"CIRCLE_NODE_TOTAL"
+	"CIRCLE_PREVIOUS_BUILD_NUM"
+	"CIRCLE_PROJECT_REPONAME"
+	"CIRCLE_PROJECT_USERNAME"
+	"CIRCLE_REPOSITORY_URL"
+	"CIRCLE_SHA1"
+	"CIRCLE_STAGE"
+	"CIRCLE_USERNAME"
+	"CIRCLE_WORKFLOW_ID"
+	"CIRCLE_WORKFLOW_JOB_ID"
+	"CIRCLE_WORKFLOW_UPSTREAM_JOB_IDS"
+	"CIRCLE_WORKFLOW_WORKSPACE_ID"
+    )
+fi
+
+# Configure the service account for pulling the docker image.
+function repo_root() {
+    local dir="$1"
+    while [[ ! -d "${dir}/.git" ]]; do
+	dir="$(dirname "$dir")"
+    done
+    echo "${dir}"
+}
+
+# Detect the project root. In CI builds, we assume the script is in
+# the git tree and traverse from there, otherwise, traverse from `pwd`
+# to find `.git` directory.
+if [[ "${RUNNING_IN_CI:-}" == "true" ]]; then
+    PROGRAM_PATH="$(realpath "$0")"
+    PROGRAM_DIR="$(dirname "${PROGRAM_PATH}")"
+    PROJECT_ROOT="$(repo_root "${PROGRAM_DIR}")"
+else
+    PROJECT_ROOT="$(repo_root $(pwd))"
+fi
+
+log_yellow "Changing to the project root: ${PROJECT_ROOT}."
+cd "${PROJECT_ROOT}"
+
+# To support relative path for `TRAMPOLINE_SERVICE_ACCOUNT`, we need
+# to use this environment variable in `PROJECT_ROOT`.
+if [[ -n "${TRAMPOLINE_SERVICE_ACCOUNT:-}" ]]; then
+
+    mkdir -p "${tmpdir}/gcloud"
+    gcloud_config_dir="${tmpdir}/gcloud"
+
+    log_yellow "Using isolated gcloud config: ${gcloud_config_dir}."
+    export CLOUDSDK_CONFIG="${gcloud_config_dir}"
+
+    log_yellow "Using ${TRAMPOLINE_SERVICE_ACCOUNT} for authentication."
+    gcloud auth activate-service-account \
+	   --key-file "${TRAMPOLINE_SERVICE_ACCOUNT}"
+    log_yellow "Configuring Container Registry access"
+    gcloud auth configure-docker --quiet
+fi
+
+required_envvars=(
+    # The basic trampoline configurations.
+    "TRAMPOLINE_IMAGE"
+    "TRAMPOLINE_BUILD_FILE"
+)
+
+if [[ -f "${PROJECT_ROOT}/.trampolinerc" ]]; then
+    source "${PROJECT_ROOT}/.trampolinerc"
+fi
+
+log_yellow "Checking environment variables."
+for e in "${required_envvars[@]}"
+do
+    if [[ -z "${!e:-}" ]]; then
+	log "Missing ${e} env var. Aborting."
+	exit 1
+    fi
+done
+
+# We want to support legacy style TRAMPOLINE_BUILD_FILE used with V1
+# script: e.g. "github/repo-name/.kokoro/run_tests.sh"
+TRAMPOLINE_BUILD_FILE="${TRAMPOLINE_BUILD_FILE#github/*/}"
+log_yellow "Using TRAMPOLINE_BUILD_FILE: ${TRAMPOLINE_BUILD_FILE}"
+
+# ignore error on docker operations and test execution
+set +e
+
+log_yellow "Preparing Docker image."
+# We only download the docker image in CI builds.
+if [[ "${RUNNING_IN_CI:-}" == "true" ]]; then
+    # Download the docker image specified by `TRAMPOLINE_IMAGE`
+
+    # We may want to add --max-concurrent-downloads flag.
+
+    log_yellow "Start pulling the Docker image: ${TRAMPOLINE_IMAGE}."
+    if docker pull "${TRAMPOLINE_IMAGE}"; then
+	log_green "Finished pulling the Docker image: ${TRAMPOLINE_IMAGE}."
+	has_image="true"
+    else
+	log_red "Failed pulling the Docker image: ${TRAMPOLINE_IMAGE}."
+	has_image="false"
+    fi
+else
+    # For local run, check if we have the image.
+    if docker images "${TRAMPOLINE_IMAGE}:latest" | grep "${TRAMPOLINE_IMAGE}"; then
+	has_image="true"
+    else
+	has_image="false"
+    fi
+fi
+
+
+# The default user for a Docker container has uid 0 (root). To avoid
+# creating root-owned files in the build directory we tell docker to
+# use the current user ID.
+user_uid="$(id -u)"
+user_gid="$(id -g)"
+user_name="$(id -un)"
+
+# To allow docker in docker, we add the user to the docker group in
+# the host os.
+docker_gid=$(cut -d: -f3 < <(getent group docker))
+
+update_cache="false"
+if [[ "${TRAMPOLINE_DOCKERFILE:-none}" != "none" ]]; then
+    # Build the Docker image from the source.
+    context_dir=$(dirname "${TRAMPOLINE_DOCKERFILE}")
+    docker_build_flags=(
+	"-f" "${TRAMPOLINE_DOCKERFILE}"
+	"-t" "${TRAMPOLINE_IMAGE}"
+	"--build-arg" "UID=${user_uid}"
+	"--build-arg" "USERNAME=${user_name}"
+    )
+    if [[ "${has_image}" == "true" ]]; then
+	docker_build_flags+=("--cache-from" "${TRAMPOLINE_IMAGE}")
+    fi
+
+    log_yellow "Start building the docker image."
+    if [[ "${TRAMPOLINE_VERBOSE:-false}" == "true" ]]; then
+	echo "docker build" "${docker_build_flags[@]}" "${context_dir}"
+    fi
+
+    # ON CI systems, we want to suppress docker build logs, only
+    # output the logs when it fails.
+    if [[ "${RUNNING_IN_CI:-}" == "true" ]]; then
+	if docker build "${docker_build_flags[@]}" "${context_dir}" \
+		  > "${tmpdir}/docker_build.log" 2>&1; then
+	    if [[ "${TRAMPOLINE_VERBOSE:-}" == "true" ]]; then
+		cat "${tmpdir}/docker_build.log"
+	    fi
+
+	    log_green "Finished building the docker image."
+	    update_cache="true"
+	else
+	    log_red "Failed to build the Docker image, aborting."
+	    log_yellow "Dumping the build logs:"
+	    cat "${tmpdir}/docker_build.log"
+	    exit 1
+	fi
+    else
+	if docker build "${docker_build_flags[@]}" "${context_dir}"; then
+	    log_green "Finished building the docker image."
+	    update_cache="true"
+	else
+	    log_red "Failed to build the Docker image, aborting."
+	    exit 1
+	fi
+    fi
+else
+    if [[ "${has_image}" != "true" ]]; then
+	log_red "We do not have ${TRAMPOLINE_IMAGE} locally, aborting."
+	exit 1
+    fi
+fi
+
+# We use an array for the flags so they are easier to document.
+docker_flags=(
+    # Remove the container after it exists.
+    "--rm"
+
+    # Use the host network.
+    "--network=host"
+
+    # Run in priviledged mode. We are not using docker for sandboxing or
+    # isolation, just for packaging our dev tools.
+    "--privileged"
+
+    # Run the docker script with the user id. Because the docker image gets to
+    # write in ${PWD} you typically want this to be your user id.
+    # To allow docker in docker, we need to use docker gid on the host.
+    "--user" "${user_uid}:${docker_gid}"
+
+    # Pass down the USER.
+    "--env" "USER=${user_name}"
+
+    # Mount the project directory inside the Docker container.
+    "--volume" "${PROJECT_ROOT}:${TRAMPOLINE_WORKSPACE}"
+    "--workdir" "${TRAMPOLINE_WORKSPACE}"
+    "--env" "PROJECT_ROOT=${TRAMPOLINE_WORKSPACE}"
+
+    # Mount the temporary home directory.
+    "--volume" "${tmphome}:/h"
+    "--env" "HOME=/h"
+
+    # Allow docker in docker.
+    "--volume" "/var/run/docker.sock:/var/run/docker.sock"
+
+    # Mount the /tmp so that docker in docker can mount the files
+    # there correctly.
+    "--volume" "/tmp:/tmp"
+    # Pass down the KOKORO_GFILE_DIR and KOKORO_KEYSTORE_DIR
+    # TODO(tmatsuo): This part is not portable.
+    "--env" "TRAMPOLINE_SECRET_DIR=/secrets"
+    "--volume" "${KOKORO_GFILE_DIR:-/dev/shm}:/secrets/gfile"
+    "--env" "KOKORO_GFILE_DIR=/secrets/gfile"
+    "--volume" "${KOKORO_KEYSTORE_DIR:-/dev/shm}:/secrets/keystore"
+    "--env" "KOKORO_KEYSTORE_DIR=/secrets/keystore"
+)
+
+# Add an option for nicer output if the build gets a tty.
+if [[ -t 0 ]]; then
+    docker_flags+=("-it")
+fi
+
+# Passing down env vars
+for e in "${pass_down_envvars[@]}"
+do
+    if [[ -n "${!e:-}" ]]; then
+	docker_flags+=("--env" "${e}=${!e}")
+    fi
+done
+
+# If arguments are given, all arguments will become the commands run
+# in the container, otherwise run TRAMPOLINE_BUILD_FILE.
+if [[ $# -ge 1 ]]; then
+    log_yellow "Running the given commands '" "${@:1}" "' in the container."
+    readonly commands=("${@:1}")
+    if [[ "${TRAMPOLINE_VERBOSE:-}" == "true" ]]; then
+	echo docker run "${docker_flags[@]}" "${TRAMPOLINE_IMAGE}" "${commands[@]}"
+    fi
+    docker run "${docker_flags[@]}" "${TRAMPOLINE_IMAGE}" "${commands[@]}"
+else
+    log_yellow "Running the tests in a Docker container."
+    docker_flags+=("--entrypoint=${TRAMPOLINE_BUILD_FILE}")
+    if [[ "${TRAMPOLINE_VERBOSE:-}" == "true" ]]; then
+	echo docker run "${docker_flags[@]}" "${TRAMPOLINE_IMAGE}"
+    fi
+    docker run "${docker_flags[@]}" "${TRAMPOLINE_IMAGE}"
+fi
+
+
+test_retval=$?
+
+if [[ ${test_retval} -eq 0 ]]; then
+    log_green "Build finished with ${test_retval}"
+else
+    log_red "Build finished with ${test_retval}"
+fi
+
+# Only upload it when the test passes.
+if [[ "${update_cache}" == "true" ]] && \
+       [[ $test_retval == 0 ]] && \
+       [[ "${TRAMPOLINE_IMAGE_UPLOAD:-false}" == "true" ]]; then
+    log_yellow "Uploading the Docker image."
+    if docker push "${TRAMPOLINE_IMAGE}"; then
+	log_green "Finished uploading the Docker image."
+    else
+	log_red "Failed uploading the Docker image."
+    fi
+    # Call trampoline_after_upload_hook if it's defined.
+    if function_exists trampoline_after_upload_hook; then
+	trampoline_after_upload_hook
+    fi
+
+fi
+
+exit "${test_retval}"
diff --git a/.repo-metadata.json b/.repo-metadata.json
new file mode 100644
index 0000000..9d799aa
--- /dev/null
+++ b/.repo-metadata.json
@@ -0,0 +1,11 @@
+{
+  "name": "google-auth",
+  "name_pretty": "Google Auth Python Library",
+  "client_documentation": "https://googleapis.dev/python/google-auth/latest",
+  "issue_tracker": "https://github.com/googleapis/google-auth-library-python/issues",
+  "release_level": "ga",
+  "language": "python",
+  "library_type": "AUTH",
+  "repo": "googleapis/google-auth-library-python",
+  "distribution_name": "google-auth"
+}
diff --git a/.trampolinerc b/.trampolinerc
new file mode 100644
index 0000000..0eee72a
--- /dev/null
+++ b/.trampolinerc
@@ -0,0 +1,63 @@
+# 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.
+
+# Template for .trampolinerc
+
+# Add required env vars here.
+required_envvars+=(
+)
+
+# Add env vars which are passed down into the container here.
+pass_down_envvars+=(
+    "NOX_SESSION"
+    ###############
+    # Docs builds
+    ###############
+    "STAGING_BUCKET"
+    "V2_STAGING_BUCKET"
+    ##################
+    # Samples builds
+    ##################
+    "INSTALL_LIBRARY_FROM_SOURCE"
+    "RUN_TESTS_SESSION"
+    "BUILD_SPECIFIC_GCLOUD_PROJECT"
+    # Target directories.
+    "RUN_TESTS_DIRS"
+    # The nox session to run.
+    "RUN_TESTS_SESSION"
+)
+
+# Prevent unintentional override on the default image.
+if [[ "${TRAMPOLINE_IMAGE_UPLOAD:-false}" == "true" ]] && \
+   [[ -z "${TRAMPOLINE_IMAGE:-}" ]]; then
+   echo "Please set TRAMPOLINE_IMAGE if you want to upload the Docker image."
+   exit 1
+fi
+
+# Define the default value if it makes sense.
+if [[ -z "${TRAMPOLINE_IMAGE_UPLOAD:-}" ]]; then
+    TRAMPOLINE_IMAGE_UPLOAD=""
+fi
+
+if [[ -z "${TRAMPOLINE_IMAGE:-}" ]]; then
+    TRAMPOLINE_IMAGE=""
+fi
+
+if [[ -z "${TRAMPOLINE_DOCKERFILE:-}" ]]; then
+    TRAMPOLINE_DOCKERFILE=""
+fi
+
+if [[ -z "${TRAMPOLINE_BUILD_FILE:-}" ]]; then
+    TRAMPOLINE_BUILD_FILE=""
+fi
diff --git a/Android.bp b/Android.bp
new file mode 100644
index 0000000..734e4e9
--- /dev/null
+++ b/Android.bp
@@ -0,0 +1,33 @@
+// Copyright 2022 Google Inc. All rights reserved.
+//
+// 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.
+
+python_library {
+    name: "py-google-auth-library-python",
+    host_supported: true,
+    srcs: [
+        "google/auth/*.py",
+        "google/auth/compute_engine/*.py",
+        "google/auth/crypt/*.py",
+        "google/auth/transport/*.py",
+        "google/oauth2/*.py",
+    ],
+    version: {
+        py2: {
+            enabled: true,
+        },
+        py3: {
+            enabled: true,
+        },
+    },
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..73440f7
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,894 @@
+# Changelog
+
+[PyPI History][1]
+
+[1]: https://pypi.org/project/google-auth/#history
+
+### [2.3.3](https://www.github.com/googleapis/google-auth-library-python/compare/v2.3.2...v2.3.3) (2021-11-01)
+
+
+### Bug Fixes
+
+* add fetch_id_token_credentials ([#866](https://www.github.com/googleapis/google-auth-library-python/issues/866)) ([8f1e9cf](https://www.github.com/googleapis/google-auth-library-python/commit/8f1e9cfd56dbaae0dff64499e1d0cf55abc5b97e))
+* fix error in sign_bytes ([#905](https://www.github.com/googleapis/google-auth-library-python/issues/905)) ([ef31284](https://www.github.com/googleapis/google-auth-library-python/commit/ef3128474431b07d1d519209ea61622bc245ce91))
+* use 'int.to_bytes' and 'int.from_bytes' for py3 ([#904](https://www.github.com/googleapis/google-auth-library-python/issues/904)) ([bd0ccc5](https://www.github.com/googleapis/google-auth-library-python/commit/bd0ccc5fe77d55f7a19f5278d6b60587c393ee3c))
+
+### [2.3.2](https://www.github.com/googleapis/google-auth-library-python/compare/v2.3.1...v2.3.2) (2021-10-26)
+
+
+### Bug Fixes
+
+* add clock_skew_in_seconds to verify_token functions ([#894](https://www.github.com/googleapis/google-auth-library-python/issues/894)) ([8e95c1e](https://www.github.com/googleapis/google-auth-library-python/commit/8e95c1e458793593972b6b05a355aaeaecd31670))
+
+### [2.3.1](https://www.github.com/googleapis/google-auth-library-python/compare/v2.3.0...v2.3.1) (2021-10-21)
+
+
+### Bug Fixes
+
+* add back python 2.7 for gcloud usage only ([#892](https://www.github.com/googleapis/google-auth-library-python/issues/892)) ([5bd5ccf](https://www.github.com/googleapis/google-auth-library-python/commit/5bd5ccf7cf229f033c7152ce0b650a40feb25f81))
+
+
+### Documentation
+
+* Fix formatting of `GCE_METADATA_HOST` ([#890](https://www.github.com/googleapis/google-auth-library-python/issues/890)) ([e2b3c98](https://www.github.com/googleapis/google-auth-library-python/commit/e2b3c98cd8c67b702be1b711c06ee7b9bbedb8ba))
+
+## [2.3.0](https://www.github.com/googleapis/google-auth-library-python/compare/v2.2.1...v2.3.0) (2021-10-07)
+
+
+### Features
+
+* add support for Python 3.10 ([#882](https://www.github.com/googleapis/google-auth-library-python/issues/882)) ([19d41f8](https://www.github.com/googleapis/google-auth-library-python/commit/19d41f8ec94ab0148d2f09a5d560ae237a87ffdb))
+
+
+### Bug Fixes
+
+* ADC with impersonated workforce pools ([#877](https://www.github.com/googleapis/google-auth-library-python/issues/877)) ([10bd9fb](https://www.github.com/googleapis/google-auth-library-python/commit/10bd9fbecd462435246afa46fd666a2836cd9e89))
+
+### [2.2.1](https://www.github.com/googleapis/google-auth-library-python/compare/v2.2.0...v2.2.1) (2021-09-28)
+
+
+### Bug Fixes
+
+* disable self signed jwt for domain wide delegation ([#873](https://www.github.com/googleapis/google-auth-library-python/issues/873)) ([0cd15e2](https://www.github.com/googleapis/google-auth-library-python/commit/0cd15e2ae20f7caddf9eb2d069064058d3c14ad7))
+
+## [2.2.0](https://www.github.com/googleapis/google-auth-library-python/compare/v2.1.0...v2.2.0) (2021-09-21)
+
+
+### Features
+
+* add support for workforce pool credentials ([#868](https://www.github.com/googleapis/google-auth-library-python/issues/868)) ([993bab2](https://www.github.com/googleapis/google-auth-library-python/commit/993bab2aaacf3034e09d9f0f25d36c0e815d3a29))
+
+## [2.1.0](https://www.github.com/googleapis/google-auth-library-python/compare/v2.0.2...v2.1.0) (2021-09-10)
+
+
+### Features
+
+* Improve handling of clock skew ([#858](https://www.github.com/googleapis/google-auth-library-python/issues/858)) ([45c4491](https://www.github.com/googleapis/google-auth-library-python/commit/45c4491fb971c9edf590b27b9e271b7a23a1bba6))
+
+
+### Bug Fixes
+
+* add SAML challenge to reauth ([#819](https://www.github.com/googleapis/google-auth-library-python/issues/819)) ([13aed5f](https://www.github.com/googleapis/google-auth-library-python/commit/13aed5ffe3ba435004ab48202462452f04d7cb29))
+* disable warning if quota project id provided to auth.default() ([#856](https://www.github.com/googleapis/google-auth-library-python/issues/856)) ([11ebaeb](https://www.github.com/googleapis/google-auth-library-python/commit/11ebaeb9d7c0862916154cfb810238574507629a))
+* rename CLOCK_SKEW and separate client/server user case ([#863](https://www.github.com/googleapis/google-auth-library-python/issues/863)) ([738611b](https://www.github.com/googleapis/google-auth-library-python/commit/738611bd2914f0fd5fa8b49b65f56ef321829c85))
+
+### [2.0.2](https://www.github.com/googleapis/google-auth-library-python/compare/v2.0.1...v2.0.2) (2021-08-25)
+
+
+### Bug Fixes
+
+* use 'int.to_bytes' rather than deprecated crypto wrapper ([#848](https://www.github.com/googleapis/google-auth-library-python/issues/848)) ([b79b554](https://www.github.com/googleapis/google-auth-library-python/commit/b79b55407b31933c9a8fe6de01478fa00a33fa2b))
+* use int.from_bytes ([#846](https://www.github.com/googleapis/google-auth-library-python/issues/846)) ([466aed9](https://www.github.com/googleapis/google-auth-library-python/commit/466aed99f5c2ba15d2036fa21cc83b3f0fc22639))
+
+### [2.0.1](https://www.github.com/googleapis/google-auth-library-python/compare/v2.0.0...v2.0.1) (2021-08-17)
+
+
+### Bug Fixes
+
+* normalize AWS paths correctly on windows ([#842](https://www.github.com/googleapis/google-auth-library-python/issues/842)) ([4e0fb1c](https://www.github.com/googleapis/google-auth-library-python/commit/4e0fb1cee78ee56b878b6e12be3b3c58df242b05))
+
+## [2.0.0](https://www.github.com/googleapis/google-auth-library-python/compare/v2.0.0-b1...v2.0.0) (2021-08-16)
+
+
+### âš  BREAKING CHANGES
+* drop support for Python 2.7 ([#778](https://www.github.com/googleapis/google-auth-library-python/issues/778)) ([560cf1e](https://www.github.com/googleapis/google-auth-library-python/commit/560cf1ed02a900436c5d9e0a0fb3f94b5fd98c55))
+
+
+### Features
+
+* service account is able to use a private token endpoint ([#835](https://www.github.com/googleapis/google-auth-library-python/issues/835)) ([20b817a](https://www.github.com/googleapis/google-auth-library-python/commit/20b817af8e202b0331998e5abde4e2a5aab51f9a))
+
+
+### Bug Fixes
+
+* downscoping documentation bugs ([#830](https://www.github.com/googleapis/google-auth-library-python/issues/830)) ([da8bb13](https://www.github.com/googleapis/google-auth-library-python/commit/da8bb13c1349e771ffc2e125256030495c53d956))
+* Fix missing space in error message. ([#821](https://www.github.com/googleapis/google-auth-library-python/issues/821)) ([7b03988](https://www.github.com/googleapis/google-auth-library-python/commit/7b039888aeb6ec7691d91c9afce182b17f02b1a6))
+
+
+### Documentation
+
+* update user guide/references for downscoped creds ([#827](https://www.github.com/googleapis/google-auth-library-python/issues/827)) ([d1840dc](https://www.github.com/googleapis/google-auth-library-python/commit/d1840dcdcd03dfd7fdfa81d08da68402f6f8b658))
+
+## [2.0.0b1](https://www.github.com/googleapis/google-auth-library-python/compare/v1.34.0...v2.0.0b1) (2021-08-03)
+
+
+### âš  BREAKING CHANGES
+
+* drop support for Python 2.7 ([#778](https://www.github.com/googleapis/google-auth-library-python/issues/778)) ([560cf1e](https://www.github.com/googleapis/google-auth-library-python/commit/560cf1ed02a900436c5d9e0a0fb3f94b5fd98c55))
+
+## [1.34.0](https://www.github.com/googleapis/google-auth-library-python/compare/v1.33.1...v1.34.0) (2021-07-23)
+
+
+### Features
+
+* support refresh callable on google.oauth2.credentials.Credentials ([#812](https://www.github.com/googleapis/google-auth-library-python/issues/812)) ([ec2fb18](https://www.github.com/googleapis/google-auth-library-python/commit/ec2fb18e7f0f452fb20e43fd0bfbb788bcf7f46b))
+
+
+### Bug Fixes
+
+* do not use the GAE APIs on gen2+ runtimes ([#807](https://www.github.com/googleapis/google-auth-library-python/issues/807)) ([7f7d92d](https://www.github.com/googleapis/google-auth-library-python/commit/7f7d92d63ffee91859fc819416af78cef3baf574))
+
+### [1.33.1](https://www.github.com/googleapis/google-auth-library-python/compare/v1.33.0...v1.33.1) (2021-07-20)
+
+
+### Bug Fixes
+
+* fallback to source creds expiration in downscoped tokens ([#805](https://www.github.com/googleapis/google-auth-library-python/issues/805)) ([dfad661](https://www.github.com/googleapis/google-auth-library-python/commit/dfad66128c6ee7513e5565d39bc7b002055dd0d5))
+
+
+### Reverts
+
+* revert "feat: service account is able to use a private token endpoint ([#784](https://www.github.com/googleapis/google-auth-library-python/issues/784))" ([#808](https://www.github.com/googleapis/google-auth-library-python/issues/808)) ([d94e65c](https://www.github.com/googleapis/google-auth-library-python/commit/d94e65c0e441183403608d762b92b30b77e21eeb))
+
+## [1.33.0](https://www.github.com/googleapis/google-auth-library-python/compare/v1.32.1...v1.33.0) (2021-07-14)
+
+
+### Features
+
+* define `CredentialAccessBoundary` classes ([#793](https://www.github.com/googleapis/google-auth-library-python/issues/793)) ([d883921](https://www.github.com/googleapis/google-auth-library-python/commit/d883921ae8fdc92b2c2cf1b3a5cd389e1287eb60))
+* define `google.auth.downscoped.Credentials` class ([#801](https://www.github.com/googleapis/google-auth-library-python/issues/801)) ([2f5c3a6](https://www.github.com/googleapis/google-auth-library-python/commit/2f5c3a636192c20cf4c92c3831d1f485031d24d2))
+* service account is able to use a private token endpoint ([#784](https://www.github.com/googleapis/google-auth-library-python/issues/784)) ([0e26409](https://www.github.com/googleapis/google-auth-library-python/commit/0e264092e35ac02ad68d5d91424ecba5397daa41))
+
+
+### Bug Fixes
+
+* fix fetch_id_token credential lookup order to match adc ([#748](https://www.github.com/googleapis/google-auth-library-python/issues/748)) ([c34452e](https://www.github.com/googleapis/google-auth-library-python/commit/c34452ef450c42cfef37a1b0c548bb422302dd5d))
+
+
+### Documentation
+
+* fix code block formatting in 'user-guide.rst' ([#794](https://www.github.com/googleapis/google-auth-library-python/issues/794)) ([4fd84bd](https://www.github.com/googleapis/google-auth-library-python/commit/4fd84bdf43694af5107dc8c8b443c06ba2f61d2c))
+
+### [1.32.1](https://www.github.com/googleapis/google-auth-library-python/compare/v1.32.0...v1.32.1) (2021-06-30)
+
+
+### Bug Fixes
+
+* avoid leaking sub-session created for '_auth_request' ([#789](https://www.github.com/googleapis/google-auth-library-python/issues/789)) ([2079ab5](https://www.github.com/googleapis/google-auth-library-python/commit/2079ab5e1db464f502248ae4f9e424deeef87fb2))
+
+## [1.32.0](https://www.github.com/googleapis/google-auth-library-python/compare/v1.31.0...v1.32.0) (2021-06-16)
+
+
+### Features
+
+* allow scopes for self signed jwt ([#776](https://www.github.com/googleapis/google-auth-library-python/issues/776)) ([2cfe655](https://www.github.com/googleapis/google-auth-library-python/commit/2cfe655bba837170abc07701557a1a5e0fe3294e))
+
+## [1.31.0](https://www.github.com/googleapis/google-auth-library-python/compare/v1.30.2...v1.31.0) (2021-06-09)
+
+
+### Features
+
+* define useful properties on `google.auth.external_account.Credentials` ([#770](https://www.github.com/googleapis/google-auth-library-python/issues/770)) ([f97499c](https://www.github.com/googleapis/google-auth-library-python/commit/f97499c718af70d17c17e0c58d6381273eceabcd))
+
+
+### Bug Fixes
+
+* avoid deleting items while iterating ([#772](https://www.github.com/googleapis/google-auth-library-python/issues/772)) ([a5e6b65](https://www.github.com/googleapis/google-auth-library-python/commit/a5e6b651aa8ad407ce087fe32f40b46925bae527))
+
+### [1.30.2](https://www.github.com/googleapis/google-auth-library-python/compare/v1.30.1...v1.30.2) (2021-06-03)
+
+
+### Bug Fixes
+
+* **dependencies:** add urllib3 and requests to aiohttp extra ([#755](https://www.github.com/googleapis/google-auth-library-python/issues/755)) ([a923442](https://www.github.com/googleapis/google-auth-library-python/commit/a9234423cb2b69068fc0d30a5a0ee86a599ab8b7))
+* enforce constraints during unit tests ([#760](https://www.github.com/googleapis/google-auth-library-python/issues/760)) ([1a6496a](https://www.github.com/googleapis/google-auth-library-python/commit/1a6496abfc17ab781bfa485dc74d0f7dbbe0c44b)), closes [#759](https://www.github.com/googleapis/google-auth-library-python/issues/759)
+* session object was never used in aiohttp request ([#700](https://www.github.com/googleapis/google-auth-library-python/issues/700)) ([#701](https://www.github.com/googleapis/google-auth-library-python/issues/701)) ([09e0389](https://www.github.com/googleapis/google-auth-library-python/commit/09e0389db72cc9d6c5dde34864cb54d717dc0b92))
+
+### [1.30.1](https://www.github.com/googleapis/google-auth-library-python/compare/v1.30.0...v1.30.1) (2021-05-20)
+
+
+### Bug Fixes
+
+* allow user to customize context aware metadata path in _mtls_helper ([#754](https://www.github.com/googleapis/google-auth-library-python/issues/754)) ([e697687](https://www.github.com/googleapis/google-auth-library-python/commit/e6976879b392508c022610ab3ea2ea55c7089c63))
+* fix function name in signing error message ([#751](https://www.github.com/googleapis/google-auth-library-python/issues/751)) ([e9ca25f](https://www.github.com/googleapis/google-auth-library-python/commit/e9ca25fa39a112cc1a376388ab47a4e1b3ea746c))
+
+## [1.30.0](https://www.github.com/googleapis/google-auth-library-python/compare/v1.29.0...v1.30.0) (2021-04-23)
+
+
+### Features
+
+* add reauth support to async user credentials for gcloud ([#738](https://www.github.com/googleapis/google-auth-library-python/issues/738)) ([9e10823](https://www.github.com/googleapis/google-auth-library-python/commit/9e1082366d113286bc063051fd76b4799791d943)). This internal feature is for gcloud developers only. 
+
+## [1.29.0](https://www.github.com/googleapis/google-auth-library-python/compare/v1.28.1...v1.29.0) (2021-04-15)
+
+
+### Features
+
+* add reauth feature to user credentials for gcloud ([#727](https://www.github.com/googleapis/google-auth-library-python/issues/727)) ([82293fe](https://www.github.com/googleapis/google-auth-library-python/commit/82293fe2caaf5258babb5df1cff0a5ddc9e44b38)). This internal feature is for gcloud developers only.
+
+
+### Bug Fixes
+
+* Allow multiple audiences for id_token.verify_token ([#733](https://www.github.com/googleapis/google-auth-library-python/issues/733)) ([56c3946](https://www.github.com/googleapis/google-auth-library-python/commit/56c394680ac6dfc07c611a9eb1e030e32edd4fe1))
+
+### [1.28.1](https://www.github.com/googleapis/google-auth-library-python/compare/v1.28.0...v1.28.1) (2021-04-08)
+
+
+### Bug Fixes
+
+* support custom alg in jwt header for signing ([#729](https://www.github.com/googleapis/google-auth-library-python/issues/729)) ([0a83706](https://www.github.com/googleapis/google-auth-library-python/commit/0a83706c9d65f7d5a30ea3b42c5beac269ed2a25))
+
+## [1.28.0](https://www.github.com/googleapis/google-auth-library-python/compare/v1.27.1...v1.28.0) (2021-03-16)
+
+
+### Features
+
+* allow the AWS_DEFAULT_REGION environment variable ([#721](https://www.github.com/googleapis/google-auth-library-python/issues/721)) ([199da47](https://www.github.com/googleapis/google-auth-library-python/commit/199da4781029916dc075738ec7bd173bd89abe54))
+* expose library version at `google.auth.__version` ([#683](https://www.github.com/googleapis/google-auth-library-python/issues/683)) ([a2cbc32](https://www.github.com/googleapis/google-auth-library-python/commit/a2cbc3245460e1ae1d310de6a2a4007d5a3a06b7))
+
+
+### Bug Fixes
+
+* fix unit tests so they can work in g3 ([#714](https://www.github.com/googleapis/google-auth-library-python/issues/714)) ([d80c85f](https://www.github.com/googleapis/google-auth-library-python/commit/d80c85f285ae1a44ddc5a5d94a66e065a79f6d19))
+
+### [1.27.1](https://www.github.com/googleapis/google-auth-library-python/compare/v1.27.0...v1.27.1) (2021-02-26)
+
+
+### Bug Fixes
+
+* ignore gcloud warning when getting project id ([#708](https://www.github.com/googleapis/google-auth-library-python/issues/708)) ([3f2f3ea](https://www.github.com/googleapis/google-auth-library-python/commit/3f2f3eaf09006d3d0ec9c030d359114238479279))
+* use gcloud creds flow ([#705](https://www.github.com/googleapis/google-auth-library-python/issues/705)) ([333cb76](https://www.github.com/googleapis/google-auth-library-python/commit/333cb765b52028329ec3ca04edf32c5764b1db68))
+
+## [1.27.0](https://www.github.com/googleapis/google-auth-library-python/compare/v1.26.1...v1.27.0) (2021-02-16)
+
+
+### Features
+
+* workload identity federation support ([#698](https://www.github.com/googleapis/google-auth-library-python/issues/698)) ([d4d7f38](https://www.github.com/googleapis/google-auth-library-python/commit/d4d7f3815e0cea3c9f39a5204a4f001de99568e9))
+
+
+### Bug Fixes
+
+* add pyopenssl as extra dependency ([#697](https://www.github.com/googleapis/google-auth-library-python/issues/697)) ([aeab5d0](https://www.github.com/googleapis/google-auth-library-python/commit/aeab5d07c5538f3d8cce817df24199534572b97d))
+
+### [1.26.1](https://www.github.com/googleapis/google-auth-library-python/compare/v1.26.0...v1.26.1) (2021-02-11)
+
+
+### Documentation
+
+* fix a typo in the user guide (avaiable -> available) ([#680](https://www.github.com/googleapis/google-auth-library-python/issues/680)) ([684457a](https://www.github.com/googleapis/google-auth-library-python/commit/684457afd3f81892e12d983a61672d7ea9bbe296))
+
+### Bug Fixes
+
+* revert workload identity federation support ([#691](https://github.com/googleapis/google-auth-library-python/pull/691))
+
+## [1.26.0](https://www.github.com/googleapis/google-auth-library-python/compare/v1.25.0...v1.26.0) (2021-02-09)
+
+
+### Features
+
+* workload identity federation support ([#686](https://www.github.com/googleapis/google-auth-library-python/issues/686)) ([5dcd2b1](https://www.github.com/googleapis/google-auth-library-python/commit/5dcd2b1bdd9d21522636d959cffc49ee29dda88f))
+
+## [1.25.0](https://www.github.com/googleapis/google-auth-library-python/compare/v1.24.0...v1.25.0) (2021-02-03)
+
+
+### Features
+
+* support self-signed jwt in requests and urllib3 transports ([#679](https://www.github.com/googleapis/google-auth-library-python/issues/679)) ([7a94acb](https://www.github.com/googleapis/google-auth-library-python/commit/7a94acb50e75fe0a51688e0f968bca3fa9bd9082))
+* use self-signed jwt for service account ([#665](https://www.github.com/googleapis/google-auth-library-python/issues/665)) ([bf5ce0c](https://www.github.com/googleapis/google-auth-library-python/commit/bf5ce0c56c10f655ced6630653f0f2ad47fcceeb))
+
+## [1.24.0](https://www.github.com/googleapis/google-auth-library-python/compare/v1.23.0...v1.24.0) (2020-12-11)
+
+
+### Features
+
+* add Python 3.9 support, drop Python 3.5 support ([#655](https://www.github.com/googleapis/google-auth-library-python/issues/655)) ([6de753d](https://www.github.com/googleapis/google-auth-library-python/commit/6de753d585254c813b3e6cbde27bf5466261ba10)), closes [#654](https://www.github.com/googleapis/google-auth-library-python/issues/654)
+
+
+### Bug Fixes
+
+* avoid losing the original '_include_email' parameter in impersonated credentials ([#626](https://www.github.com/googleapis/google-auth-library-python/issues/626)) ([fd9b5b1](https://www.github.com/googleapis/google-auth-library-python/commit/fd9b5b10c80950784bd37ee56e32c505acb5078d))
+
+
+### Documentation
+
+* fix typo in import ([#651](https://www.github.com/googleapis/google-auth-library-python/issues/651)) ([3319ea8](https://www.github.com/googleapis/google-auth-library-python/commit/3319ea8ae876c73a94f51237b3bbb3f5df2aef89)), closes [#650](https://www.github.com/googleapis/google-auth-library-python/issues/650)
+
+## [1.23.0](https://www.github.com/googleapis/google-auth-library-python/compare/v1.22.1...v1.23.0) (2020-10-29)
+
+
+### Features
+
+* Add custom scopes for access tokens from the metadata service ([#633](https://www.github.com/googleapis/google-auth-library-python/issues/633)) ([0323cf3](https://www.github.com/googleapis/google-auth-library-python/commit/0323cf390b16e8483660ac88775e8ea4e7f7702d))
+
+
+### Bug Fixes
+
+* **deps:** Revert "fix: pin 'aoihttp < 3.7.0dev' ([#634](https://www.github.com/googleapis/google-auth-library-python/issues/634))" ([#632](https://www.github.com/googleapis/google-auth-library-python/issues/632)) ([#640](https://www.github.com/googleapis/google-auth-library-python/issues/640)) ([b790e65](https://www.github.com/googleapis/google-auth-library-python/commit/b790e6535cc37591b23866027a426cde312e07c1))
+* pin 'aoihttp < 3.7.0dev' ([#634](https://www.github.com/googleapis/google-auth-library-python/issues/634)) ([05f9524](https://www.github.com/googleapis/google-auth-library-python/commit/05f95246fab928fe2f445781117eeac8088497fb))
+* remove checks for ancient versions of Cryptography ([#596](https://www.github.com/googleapis/google-auth-library-python/issues/596)) ([6407258](https://www.github.com/googleapis/google-auth-library-python/commit/6407258956ec42e3b722418cb7f366e5ae9272ec)), closes [/github.com/googleapis/google-auth-library-python/issues/595#issuecomment-683903062](https://www.github.com/googleapis//github.com/googleapis/google-auth-library-python/issues/595/issues/issuecomment-683903062)
+
+### [1.22.1](https://www.github.com/googleapis/google-auth-library-python/compare/v1.22.0...v1.22.1) (2020-10-05)
+
+
+### Bug Fixes
+
+* move aiohttp to extra as it is currently internal surface ([#619](https://www.github.com/googleapis/google-auth-library-python/issues/619)) ([a924011](https://www.github.com/googleapis/google-auth-library-python/commit/a9240111e7af29338624d98ee10aed31462f4d19)), closes [#618](https://www.github.com/googleapis/google-auth-library-python/issues/618)
+
+## [1.22.0](https://www.github.com/googleapis/google-auth-library-python/compare/v1.21.3...v1.22.0) (2020-09-28)
+
+
+### Features
+
+* add asyncio based auth flow ([#612](https://www.github.com/googleapis/google-auth-library-python/issues/612)) ([7e15258](https://www.github.com/googleapis/google-auth-library-python/commit/7e1525822d51bd9ce7dffca42d71313e6e776fcd)), closes [#572](https://www.github.com/googleapis/google-auth-library-python/issues/572)
+
+### [1.21.3](https://www.github.com/googleapis/google-auth-library-python/compare/v1.21.2...v1.21.3) (2020-09-22)
+
+
+### Bug Fixes
+
+* fix expiry for `to_json()` ([#589](https://www.github.com/googleapis/google-auth-library-python/issues/589)) ([d0e0aba](https://www.github.com/googleapis/google-auth-library-python/commit/d0e0aba0a9f665268ffa1b22d44f4bd7e9b449d6)), closes [/github.com/googleapis/oauth2client/blob/master/oauth2client/client.py#L55](https://www.github.com/googleapis//github.com/googleapis/oauth2client/blob/master/oauth2client/client.py/issues/L55)
+
+### [1.21.2](https://www.github.com/googleapis/google-auth-library-python/compare/v1.21.1...v1.21.2) (2020-09-08)
+
+
+### Bug Fixes
+
+* migrate signBlob to iamcredentials.googleapis.com ([#600](https://www.github.com/googleapis/google-auth-library-python/issues/600)) ([694d83f](https://www.github.com/googleapis/google-auth-library-python/commit/694d83fd23c0e8c2fde27136d1b3f8f6db6338a6))
+
+### [1.21.1](https://www.github.com/googleapis/google-auth-library-python/compare/v1.21.0...v1.21.1) (2020-09-03)
+
+
+### Bug Fixes
+
+* dummy commit to trigger a auto release ([#597](https://www.github.com/googleapis/google-auth-library-python/issues/597)) ([d32f7df](https://www.github.com/googleapis/google-auth-library-python/commit/d32f7df4895122ef23b664672d7db3f58d9b7d36))
+
+## [1.21.0](https://www.github.com/googleapis/google-auth-library-python/compare/v1.20.1...v1.21.0) (2020-08-27)
+
+
+### Features
+
+* add GOOGLE_API_USE_CLIENT_CERTIFICATE support ([#592](https://www.github.com/googleapis/google-auth-library-python/issues/592)) ([c0c995f](https://www.github.com/googleapis/google-auth-library-python/commit/c0c995f3de237a2346b59797ee7c4d44ff2a197c))
+
+### [1.20.1](https://www.github.com/googleapis/google-auth-library-python/compare/v1.20.0...v1.20.1) (2020-08-06)
+
+
+### Bug Fixes
+
+* reduce refresh clock skew to 10 seconds ([#581](https://www.github.com/googleapis/google-auth-library-python/issues/581)) ([42321ba](https://www.github.com/googleapis/google-auth-library-python/commit/42321bafd38a8bd806f4d01bfa0eda3b5a961667))
+* set Content-Type header in the request to signBlob API to avoid Invalid JSON payload error ([#439](https://www.github.com/googleapis/google-auth-library-python/issues/439)) ([20f82e2](https://www.github.com/googleapis/google-auth-library-python/commit/20f82e22b7e8c6c7fdd29e08eaf7b4cf2abdcf37))
+
+## [1.20.0](https://www.github.com/googleapis/google-auth-library-python/compare/v1.19.2...v1.20.0) (2020-07-23)
+
+
+### Features
+
+* Add debug logging that can help with diagnosing auth lib. path ([#473](https://www.github.com/googleapis/google-auth-library-python/issues/473)) ([ecd88d4](https://www.github.com/googleapis/google-auth-library-python/commit/ecd88d4f0efc5c619ebd3e3fa7e2472f11c63452))
+* Show the transport exception that happened for GCE Metadata ([#474](https://www.github.com/googleapis/google-auth-library-python/issues/474)) ([23919bb](https://www.github.com/googleapis/google-auth-library-python/commit/23919bb60e5f9d9b73644e9a2e127d4d1dd68e8c))
+* **packaging:** add support for Python 3.8 ([#569](https://www.github.com/googleapis/google-auth-library-python/issues/569)) ([1aad54a](https://www.github.com/googleapis/google-auth-library-python/commit/1aad54af6b1d5da73d7471cdbfaf0d0b37c5fde6)), closes [#568](https://www.github.com/googleapis/google-auth-library-python/issues/568)
+
+### [1.19.2](https://www.github.com/googleapis/google-auth-library-python/compare/v1.19.1...v1.19.2) (2020-07-17)
+
+
+### Bug fixes
+ 
+* Revert "fix: migrate signBlob to iamcredentials.googleapis.com"  ([#563](https://www.github.com/googleapis/google-auth-library-python/issues/563)) ([a48b5b](https://www.github.com/googleapis/google-auth-library-python/commit/a48b5b9135b30ff06f1fe18dd9dbe92ffcf3a272))
+
+### [1.19.1](https://www.github.com/googleapis/google-auth-library-python/compare/v1.19.0...v1.19.1) (2020-07-15)
+
+
+### Bug Fixes
+
+* don't add empty quota project  ([#560](https://www.github.com/googleapis/google-auth-library-python/issues/560)) ([ab2be5d](https://www.github.com/googleapis/google-auth-library-python/commit/ab2be5de829e830979514683582c11f98fa943c7))
+
+## [1.19.0](https://www.github.com/googleapis/google-auth-library-python/compare/v1.18.0...v1.19.0) (2020-07-09)
+
+
+### Features
+
+* add quota project to base credentials class ([#546](https://www.github.com/googleapis/google-auth-library-python/issues/546)) ([3dda7b2](https://www.github.com/googleapis/google-auth-library-python/commit/3dda7b2ab88aba7941b8b5281b4acbc7db74169b))
+* check 'iss' in `verify_oauth2_token` ([#500](https://www.github.com/googleapis/google-auth-library-python/issues/500)) ([c05b8b5](https://www.github.com/googleapis/google-auth-library-python/commit/c05b8b52e3bbc096cf32e2d4bb5bd45986d3cd04))
+
+
+### Bug Fixes
+
+* migrate signBlob to iamcredentials.googleapis.com ([#553](https://www.github.com/googleapis/google-auth-library-python/issues/553)) ([038ae1b](https://www.github.com/googleapis/google-auth-library-python/commit/038ae1b78dc83e44ad39ef7ba15c607f62232087))
+
+
+### Documentation
+
+* remove 3.4 from supported versions list ([#549](https://www.github.com/googleapis/google-auth-library-python/issues/549)) ([8c84d0f](https://www.github.com/googleapis/google-auth-library-python/commit/8c84d0fb36d9eba6b319964ca0a22501efca805b))
+
+## [1.18.0](https://www.github.com/googleapis/google-auth-library-python/compare/v1.17.2...v1.18.0) (2020-06-18)
+
+
+### Features
+
+* make ``load_credentials_from_file`` a public method ([#530](https://www.github.com/googleapis/google-auth-library-python/issues/530)) ([15d5fa9](https://www.github.com/googleapis/google-auth-library-python/commit/15d5fa946177581b52a5a9eb3ca285c088f5c45d))
+
+
+### Bug Fixes
+
+* no warning if quota_project_id is given ([#537](https://www.github.com/googleapis/google-auth-library-python/issues/537)) ([f30b45a](https://www.github.com/googleapis/google-auth-library-python/commit/f30b45a9b2f824c494724548732c5ce838218c30))
+
+### [1.17.2](https://www.github.com/googleapis/google-auth-library-python/compare/v1.17.1...v1.17.2) (2020-06-12)
+
+
+### Bug Fixes
+
+* **dependencies:** Further restrict RSA versions ([#532](https://www.github.com/googleapis/google-auth-library-python/issues/532)) ([46677a0](https://www.github.com/googleapis/google-auth-library-python/commit/46677a0cb3bde6622be10061bc61daaff7a0aaca)), closes [#528](https://www.github.com/googleapis/google-auth-library-python/issues/528)
+
+### [1.17.1](https://www.github.com/googleapis/google-auth-library-python/compare/v1.17.0...v1.17.1) (2020-06-11)
+
+
+### Bug Fixes
+
+* narrow acceptable RSA versions to maintain Python 2 compatability ([#528](https://www.github.com/googleapis/google-auth-library-python/issues/528)) ([9434868](https://www.github.com/googleapis/google-auth-library-python/commit/9434868a6789464549af1d4562f62d8a899b6809))
+
+## [1.17.0](https://www.github.com/googleapis/google-auth-library-python/compare/v1.16.1...v1.17.0) (2020-06-10)
+
+
+### Features
+
+* add quota_project_id to service accounts; add with_quota_project methods ([#519](https://www.github.com/googleapis/google-auth-library-python/issues/519)) ([b12488c](https://www.github.com/googleapis/google-auth-library-python/commit/b12488cf552888299425c8009ea075511627cf08))
+
+### [1.16.1](https://www.github.com/googleapis/google-auth-library-python/compare/v1.16.0...v1.16.1) (2020-06-04)
+
+
+### Bug Fixes
+
+* fix impersonated cred exception doc ([#521](https://www.github.com/googleapis/google-auth-library-python/issues/521)) ([9d5a9a9](https://www.github.com/googleapis/google-auth-library-python/commit/9d5a9a9884fecbd698a602d2a9fd9bec6b987de7))
+* replace environment variable GCE_METADATA_ROOT with GCE_METADATA_HOST ([#433](https://www.github.com/googleapis/google-auth-library-python/issues/433)) ([8ffb4d3](https://www.github.com/googleapis/google-auth-library-python/commit/8ffb4d3e832607869026444e5a071c5f3e225fd2)), closes [#339](https://www.github.com/googleapis/google-auth-library-python/issues/339)
+
+## [1.16.0](https://www.github.com/googleapis/google-auth-library-python/compare/v1.15.0...v1.16.0) (2020-05-28)
+
+
+### Features
+
+* add helper func to for default encrypted cert ([#514](https://www.github.com/googleapis/google-auth-library-python/issues/514)) ([f282aa4](https://www.github.com/googleapis/google-auth-library-python/commit/f282aa4acc73d5b56aa7d4bb745d286c3cf1fc39))
+
+
+### Bug Fixes
+
+* fix impersonated cred for gcloud ([#516](https://www.github.com/googleapis/google-auth-library-python/issues/516)) ([eb7be3f](https://www.github.com/googleapis/google-auth-library-python/commit/eb7be3fa98ace42b3e949a8af90bbb978ae7e455))
+
+## [1.15.0](https://www.github.com/googleapis/google-auth-library-python/compare/v1.14.3...v1.15.0) (2020-05-15)
+
+
+### Features
+
+* encrypted mtls private key support ([#496](https://www.github.com/googleapis/google-auth-library-python/issues/496)) ([9dc9e9f](https://www.github.com/googleapis/google-auth-library-python/commit/9dc9e9f4ca65780b4d7f24e2c36021d2300b4006))
+
+
+### Bug Fixes
+
+* signBytes for impersonated credentials ([#506](https://www.github.com/googleapis/google-auth-library-python/issues/506)) ([ca8d98a](https://www.github.com/googleapis/google-auth-library-python/commit/ca8d98ab2e5277e53ab8df78beb1e75cdf5321e3)), closes [#338](https://www.github.com/googleapis/google-auth-library-python/issues/338)
+
+### [1.14.3](https://www.github.com/googleapis/google-auth-library-python/compare/v1.14.2...v1.14.3) (2020-05-11)
+
+
+### Bug Fixes
+
+* catch exceptions.RefreshError ([#508](https://www.github.com/googleapis/google-auth-library-python/issues/508)) ([3d672e9](https://www.github.com/googleapis/google-auth-library-python/commit/3d672e9cddd9e8c4946290ab9f90ca9009b8be69))
+
+### [1.14.2](https://www.github.com/googleapis/google-auth-library-python/compare/v1.14.1...v1.14.2) (2020-05-07)
+
+
+### Bug Fixes
+
+* support string type response.data ([#504](https://www.github.com/googleapis/google-auth-library-python/issues/504)) ([9b7228e](https://www.github.com/googleapis/google-auth-library-python/commit/9b7228ec849e311bcb4007ad3e23cf2f1e54a721))
+
+### [1.14.1](https://www.github.com/googleapis/google-auth-library-python/compare/v1.14.0...v1.14.1) (2020-04-21)
+
+
+### Bug Fixes
+
+* support es256 raw format signature ([#490](https://www.github.com/googleapis/google-auth-library-python/issues/490)) ([cf2c0a9](https://www.github.com/googleapis/google-auth-library-python/commit/cf2c0a90701ce42f47df71281ae9cdf212c28e0e))
+
+## [1.14.0](https://www.github.com/googleapis/google-auth-library-python/compare/v1.13.1...v1.14.0) (2020-04-13)
+
+
+### Features
+
+* add default client cert source util ([#486](https://www.github.com/googleapis/google-auth-library-python/issues/486)) ([ed41b49](https://www.github.com/googleapis/google-auth-library-python/commit/ed41b49e9d7ba7402b27107b7aa47eed06ac6c55))
+
+### [1.13.1](https://www.github.com/googleapis/google-auth-library-python/compare/v1.13.0...v1.13.1) (2020-04-01)
+
+
+### Bug Fixes
+
+* invalid expiry type ([#481](https://www.github.com/googleapis/google-auth-library-python/issues/481)) ([7ae9a28](https://www.github.com/googleapis/google-auth-library-python/commit/7ae9a284dae16d274bfd4d876414f08efd6c3bff))
+
+## [1.13.0](https://www.github.com/googleapis/google-auth-library-python/compare/v1.12.0...v1.13.0) (2020-04-01)
+
+
+### Features
+
+* add access token credentials ([#476](https://www.github.com/googleapis/google-auth-library-python/issues/476)) ([772dac6](https://www.github.com/googleapis/google-auth-library-python/commit/772dac6a6512230d32cb0dfae65a1a6aa9015049))
+* add fetch_id_token to support id_token adc ([#469](https://www.github.com/googleapis/google-auth-library-python/issues/469)) ([506c565](https://www.github.com/googleapis/google-auth-library-python/commit/506c565a8c3c23a78fd0f17991bc6deb6f2528a9))
+* consolidate mTLS channel errors ([#480](https://www.github.com/googleapis/google-auth-library-python/issues/480)) ([e83d446](https://www.github.com/googleapis/google-auth-library-python/commit/e83d4462f5c50f8424d9e54be32c29390115a9ed))
+* Implement ES256 for JWT verification ([#340](https://www.github.com/googleapis/google-auth-library-python/issues/340)) ([e290a3d](https://www.github.com/googleapis/google-auth-library-python/commit/e290a3dbecc4767dd25ee14574951cdb6c2157cb))
+
+## [1.12.0](https://www.github.com/googleapis/google-auth-library-python/compare/v1.11.3...v1.12.0) (2020-03-25)
+
+
+### Features
+
+* add mTLS ADC support for HTTP ([#457](https://www.github.com/googleapis/google-auth-library-python/issues/457)) ([bb9215a](https://www.github.com/googleapis/google-auth-library-python/commit/bb9215ad6dee6c1dc7f255a2e4ed7011b85bd6cf))
+* add SslCredentials class for mTLS ADC ([#448](https://www.github.com/googleapis/google-auth-library-python/issues/448)) ([dafb41f](https://www.github.com/googleapis/google-auth-library-python/commit/dafb41fae3f513ea9a4f93404f6148bee7dda202))
+* fetch id token from GCE metadata server ([#462](https://www.github.com/googleapis/google-auth-library-python/issues/462)) ([97e7700](https://www.github.com/googleapis/google-auth-library-python/commit/97e7700da031bfd80b63b1a3d2abc29c500936ef))
+
+
+### Bug Fixes
+
+* don't use threads for gRPC AuthMetadataPlugin ([#467](https://www.github.com/googleapis/google-auth-library-python/issues/467)) ([ee373f8](https://www.github.com/googleapis/google-auth-library-python/commit/ee373f88b512a38e791a1c085452c6c6da501eb6))
+* make ThreadPoolExecutor a class var ([#461](https://www.github.com/googleapis/google-auth-library-python/issues/461)) ([b526473](https://www.github.com/googleapis/google-auth-library-python/commit/b5264730603947295cc97ecff2f6aef84aa3d6e9))
+
+### [1.11.3](https://www.github.com/googleapis/google-auth-library-python/compare/v1.11.2...v1.11.3) (2020-03-13)
+
+
+### Bug Fixes
+
+* fix the scopes so test can pass for a local run ([#450](https://www.github.com/googleapis/google-auth-library-python/issues/450)) ([b2dd77f](https://www.github.com/googleapis/google-auth-library-python/commit/b2dd77fe4a538e1d165fc9d859c9a299f6832cda))
+* only add IAM scope to credentials that can change scopes ([#451](https://www.github.com/googleapis/google-auth-library-python/issues/451)) ([82e224b](https://www.github.com/googleapis/google-auth-library-python/commit/82e224b0854950a5607cd028edbcbcdc3e9e6505))
+
+### [1.11.2](https://www.github.com/googleapis/google-auth-library-python/compare/v1.11.1...v1.11.2) (2020-02-14)
+
+
+### Reverts
+
+* Revert "fix: update `_GOOGLE_OAUTH2_CERTS_URL` (#365)" (#444) ([901c259](https://www.github.com/googleapis/google-auth-library-python/commit/901c259b1764f5a305a542cbae14d926ba7a57db)), closes [#365](https://www.github.com/googleapis/google-auth-library-python/issues/365) [#444](https://www.github.com/googleapis/google-auth-library-python/issues/444)
+
+### [1.11.1](https://www.github.com/googleapis/google-auth-library-python/compare/v1.11.0...v1.11.1) (2020-02-13)
+
+
+### Bug Fixes
+
+* compute engine id token credentials "with_target_audience" method ([#438](https://www.github.com/googleapis/google-auth-library-python/issues/438)) ([bc0ec93](https://www.github.com/googleapis/google-auth-library-python/commit/bc0ec93dc66fdcaa6a82222386623fa44f24ddfe))
+* update `_GOOGLE_OAUTH2_CERTS_URL` ([#365](https://www.github.com/googleapis/google-auth-library-python/issues/365)) ([054db75](https://www.github.com/googleapis/google-auth-library-python/commit/054db75734756b0e82e7984ca07fa80025edc908))
+
+## [1.11.0](https://www.github.com/googleapis/google-auth-library-python/compare/v1.10.2...v1.11.0) (2020-01-23)
+
+
+### Features
+
+* add non-None default timeout to AuthorizedSession.request() ([#435](https://www.github.com/googleapis/google-auth-library-python/issues/435)) ([d274a3a](https://www.github.com/googleapis/google-auth-library-python/commit/d274a3a2b3f913bc2cab4ca51f9c7fdef94b8f31)), closes [#434](https://www.github.com/googleapis/google-auth-library-python/issues/434) [googleapis/google-cloud-python#10182](https://www.github.com/googleapis/google-cloud-python/issues/10182)
+* distinguish transport and execution time timeouts ([#424](https://www.github.com/googleapis/google-auth-library-python/issues/424)) ([52a733d](https://www.github.com/googleapis/google-auth-library-python/commit/52a733d604528fa86d05321bb74241a43aea4211)), closes [#423](https://github.com/googleapis/google-auth-library-python/issues/423)
+
+### [1.10.2](https://www.github.com/googleapis/google-auth-library-python/compare/v1.10.1...v1.10.2) (2020-01-18)
+
+
+### Bug Fixes
+
+* make collections import compatible across Python versions ([#419](https://www.github.com/googleapis/google-auth-library-python/issues/419)) ([c5a3395](https://www.github.com/googleapis/google-auth-library-python/commit/c5a3395b8781e14c4566cf0e476b234d6a1c1224)), closes [#418](https://www.github.com/googleapis/google-auth-library-python/issues/418)
+
+### [1.10.1](https://www.github.com/googleapis/google-auth-library-python/compare/v1.10.0...v1.10.1) (2020-01-10)
+
+
+### Bug Fixes
+
+* **google.auth.compute_engine.metadata:** add retry to google.auth.compute_engine._metadata.get() ([#398](https://www.github.com/googleapis/google-auth-library-python/issues/398)) ([af29c1a](https://www.github.com/googleapis/google-auth-library-python/commit/af29c1a9fd9282b38867961e4053f74f018a3815)), closes [#211](https://www.github.com/googleapis/google-auth-library-python/issues/211) [#323](https://www.github.com/googleapis/google-auth-library-python/issues/323) [#323](https://www.github.com/googleapis/google-auth-library-python/issues/323) [#211](https://www.github.com/googleapis/google-auth-library-python/issues/211)
+* always pass body of type bytes to `google.auth.transport.Request` ([#421](https://www.github.com/googleapis/google-auth-library-python/issues/421)) ([a57a770](https://www.github.com/googleapis/google-auth-library-python/commit/a57a7708cfea635b5030f8c7ba10c967715f9a87)), closes [#318](https://www.github.com/googleapis/google-auth-library-python/issues/318)
+
+## [1.10.0](https://www.github.com/googleapis/google-auth-library-python/compare/v1.9.0...v1.10.0) (2019-12-18)
+
+
+### Features
+
+* send quota project id in x-goog-user-project for OAuth2 credentials ([#412](https://www.github.com/googleapis/google-auth-library-python/issues/412)) ([32d71a5](https://www.github.com/googleapis/google-auth-library-python/commit/32d71a5858435af0818a705b754404882bb7bb9e)), closes [#400](https://www.github.com/googleapis/google-auth-library-python/issues/400)
+
+## [1.9.0](https://www.github.com/googleapis/google-auth-library-python/compare/v1.8.2...v1.9.0) (2019-12-12)
+
+
+### Features
+
+* add timeout parameter to `AuthorizedSession.request()` ([#406](https://www.github.com/googleapis/google-auth-library-python/issues/406)) ([d86d7b8](https://www.github.com/googleapis/google-auth-library-python/commit/d86d7b8c43df152765c7fc59a54015361b46dcde))
+
+### [1.8.2](https://www.github.com/googleapis/google-auth-library-python/compare/v1.8.1...v1.8.2) (2019-12-11)
+
+
+### Bug Fixes
+
+* revert "feat: send quota project id in x-goog-user-project header for OAuth2 credentials ([#400](https://www.github.com/googleapis/google-auth-library-python/issues/400))" ([#407](https://www.github.com/googleapis/google-auth-library-python/issues/407)) ([25ea942](https://www.github.com/googleapis/google-auth-library-python/commit/25ea942cef4378ff22adf235dd1baf1ca0d595f8))
+
+### [1.8.1](https://www.github.com/googleapis/google-auth-library-python/compare/v1.8.0...v1.8.1) (2019-12-09)
+
+
+### Bug Fixes
+
+* revert "feat: add timeout to AuthorizedSession.request() ([#397](https://www.github.com/googleapis/google-auth-library-python/issues/397))" ([#401](https://www.github.com/googleapis/google-auth-library-python/issues/401)) ([451ecbd](https://www.github.com/googleapis/google-auth-library-python/commit/451ecbd48a910348bbf7a7b38162a044fad6e6e1))
+
+## [1.8.0](https://www.github.com/googleapis/google-auth-library-python/compare/v1.7.2...v1.8.0) (2019-12-09)
+
+
+### Features
+
+* add `to_json` method to google.oauth2.credentials.Credentials ([#367](https://www.github.com/googleapis/google-auth-library-python/issues/367)) ([bfb1f8c](https://www.github.com/googleapis/google-auth-library-python/commit/bfb1f8cc8a706ce5ca2a14886c920ca2220ec349))
+* add timeout to AuthorizedSession.request() ([#397](https://www.github.com/googleapis/google-auth-library-python/issues/397)) ([381dd40](https://www.github.com/googleapis/google-auth-library-python/commit/381dd400911d29926ffbf04e0f2ba53ef7bb997e))
+* send quota project id in x-goog-user-project header for OAuth2 credentials ([#400](https://www.github.com/googleapis/google-auth-library-python/issues/400)) ([ab3dc1e](https://www.github.com/googleapis/google-auth-library-python/commit/ab3dc1e26f5240ea3456de364c7c5cb8f40f9583))
+
+### [1.7.2](https://www.github.com/googleapis/google-auth-library-python/compare/v1.7.1...v1.7.2) (2019-12-02)
+
+
+### Bug Fixes
+
+* in token endpoint request, do not decode the response data if it is not encoded ([#393](https://www.github.com/googleapis/google-auth-library-python/issues/393)) ([3b5d3e2](https://www.github.com/googleapis/google-auth-library-python/commit/3b5d3e2192ce0cdc97854a1d70d5e382e454275c))
+* make gRPC auth plugin non-blocking + add default timeout value for requests transport ([#390](https://www.github.com/googleapis/google-auth-library-python/issues/390)) ([0c33e9c](https://www.github.com/googleapis/google-auth-library-python/commit/0c33e9c0fe4f87fa46c8f1a5afe725a467ac5fcc)), closes [#351](https://www.github.com/googleapis/google-auth-library-python/issues/351)
+
+### [1.7.1](https://www.github.com/googleapis/google-auth-library-python/compare/v1.7.0...v1.7.1) (2019-11-13)
+
+
+### Bug Fixes
+
+* change 'internal_failure' condition to also use `error' field ([#387](https://www.github.com/googleapis/google-auth-library-python/issues/387)) ([46bb58e](https://www.github.com/googleapis/google-auth-library-python/commit/46bb58e694716908a5ed00f05dbb794cdec667dd))
+
+## 1.7.0
+
+10-30-2019 17:11 PDT
+
+
+### Implementation Changes
+- Add retry loop  for fetching authentication token if any 'Internal Failure' occurs ([#368](https://github.com/googleapis/google-auth-library-python/pull/368))
+- Use cls parameter instead of class ([#341](https://github.com/googleapis/google-auth-library-python/pull/341))
+
+### New Features
+- Add support for `impersonated_credentials.Sign`, `IDToken` ([#348](https://github.com/googleapis/google-auth-library-python/pull/348))
+- Add downscoping to OAuth2 credentials ([#309](https://github.com/googleapis/google-auth-library-python/pull/309))
+
+### Dependencies
+- Update dependency cachetools to v3 ([#357](https://github.com/googleapis/google-auth-library-python/pull/357))
+- Update dependency rsa to v4 ([#358](https://github.com/googleapis/google-auth-library-python/pull/358))
+- Set an upper bound on dependencies version ([#352](https://github.com/googleapis/google-auth-library-python/pull/352))
+- Require a minimum version of setuptools ([#322](https://github.com/googleapis/google-auth-library-python/pull/322))
+
+### Documentation
+- Add busunkim96 as maintainer ([#373](https://github.com/googleapis/google-auth-library-python/pull/373))
+- Update user-guide.rst ([#337](https://github.com/googleapis/google-auth-library-python/pull/337))
+- Fix typo in jwt docs ([#332](https://github.com/googleapis/google-auth-library-python/pull/332))
+- Clarify which SA has Token Creator role ([#330](https://github.com/googleapis/google-auth-library-python/pull/330))
+
+### Internal / Testing Changes
+- Change 'name' to distribution name ([#379](https://github.com/googleapis/google-auth-library-python/pull/379))
+- Fix system tests, move to Kokoro ([#372](https://github.com/googleapis/google-auth-library-python/pull/372))
+- Blacken ([#375](https://github.com/googleapis/google-auth-library-python/pull/375))
+- Rename nox.py -> noxfile.py ([#369](https://github.com/googleapis/google-auth-library-python/pull/369))
+- Add initial renovate config ([#356](https://github.com/googleapis/google-auth-library-python/pull/356))
+- Use new pytest api to keep building with pytest 5 ([#353](https://github.com/googleapis/google-auth-library-python/pull/353))
+
+
+## 1.6.3
+
+02-15-2019 9:31 PST
+
+### Implementation Changes
+
+- follow rfc 7515 : strip padding from JWS segments  ([#324](https://github.com/googleapis/google-auth-library-python/pull/324))
+- Add retry to `_metadata.ping()` ([#323](https://github.com/googleapis/google-auth-library-python/pull/323))
+
+## 1.6.2
+
+12-17-2018 10:51 PST
+
+### Documentation
+
+- Announce deprecation of Python 2.7 ([#311](https://github.com/googleapis/google-auth-library-python/pull/311))
+- Link all the PRs in CHANGELOG ([#307](https://github.com/googleapis/google-auth-library-python/pull/307))
+
+## 1.6.1
+
+11-12-2018 10:10 PST
+
+### Implementation Changes
+
+- Automatically refresh impersonated credentials ([#304](https://github.com/googleapis/google-auth-library-python/pull/304))
+
+## 1.6.0
+
+11-09-2018 11:07 PST
+
+### New Features
+
+- Add `google.auth.impersonated_credentials` ([#299](https://github.com/googleapis/google-auth-library-python/pull/299))
+
+### Documentation
+
+- Update link to documentation for default credentials ([#296](https://github.com/googleapis/google-auth-library-python/pull/296))
+- Update github issue templates ([#300](https://github.com/googleapis/google-auth-library-python/pull/300))
+- Remove punctuation which becomes part of the url ([#284](https://github.com/googleapis/google-auth-library-python/pull/284))
+
+### Internal / Testing Changes
+
+- Update trampoline.sh ([302](https://github.com/googleapis/google-auth-library-python/pull/302))
+- Enable static type checking with pytype ([#298](https://github.com/googleapis/google-auth-library-python/pull/298))
+- Make classifiers in setup.py an array. ([#280](https://github.com/googleapis/google-auth-library-python/pull/280))
+
+
+## 1.5.1
+
+- Fix check for error text on Python 3.7. ([#278](https://github.com/googleapis/google-auth-library-python/pull/#278))
+- Use new Auth URIs. ([#281](https://github.com/googleapis/google-auth-library-python/pull/#281))
+- Add code-of-conduct document. ([#270](https://github.com/googleapis/google-auth-library-python/pull/#270))
+- Fix some typos in test_urllib3.py ([#268](https://github.com/googleapis/google-auth-library-python/pull/#268))
+
+## 1.5.0
+
+- Warn when using user credentials from the Cloud SDK ([#266](https://github.com/googleapis/google-auth-library-python/pull/266))
+- Add compute engine-based IDTokenCredentials ([#236](https://github.com/googleapis/google-auth-library-python/pull/236))
+- Corrected some typos ([#265](https://github.com/googleapis/google-auth-library-python/pull/265))
+
+## 1.4.2
+
+- Raise a helpful exception when trying to refresh credentials without a refresh token. ([#262](https://github.com/googleapis/google-auth-library-python/pull/262))
+- Fix links to README and CONTRIBUTING in docs/index.rst. ([#260](https://github.com/googleapis/google-auth-library-python/pull/260))
+- Fix a typo in credentials.py. ([#256](https://github.com/googleapis/google-auth-library-python/pull/256))
+- Use pytest instead of py.test per upstream recommendation, #dropthedot. ([#255](https://github.com/googleapis/google-auth-library-python/pull/255))
+- Fix typo on exemple of jwt usage ([#245](https://github.com/googleapis/google-auth-library-python/pull/245))
+
+## 1.4.1
+
+- Added a check for the cryptography version before attempting to use it. ([#243](https://github.com/googleapis/google-auth-library-python/pull/243))
+
+## 1.4.0
+
+- Added `cryptography`-based RSA signer and verifier. ([#185](https://github.com/googleapis/google-auth-library-python/pull/185))
+- Added `google.oauth2.service_account.IDTokenCredentials`. ([#234](https://github.com/googleapis/google-auth-library-python/pull/234))
+- Improved documentation around ID Tokens ([#224](https://github.com/googleapis/google-auth-library-python/pull/224))
+
+## 1.3.0
+
+- Added ``google.oauth2.credentials.Credentials.from_authorized_user_file`` ([#226](https://github.com/googleapis/google-auth-library-python/pull/#226))
+- Dropped direct pyasn1 dependency in favor of letting ``pyasn1-modules`` specify the right version. ([#230](https://github.com/googleapis/google-auth-library-python/pull/#230))
+- ``default()`` now checks for the project ID environment var before warning about missing project ID. ([#227](https://github.com/googleapis/google-auth-library-python/pull/#227))
+- Fixed the docstrings for ``has_scopes()`` and ``with_scopes()``. ([#228](https://github.com/googleapis/google-auth-library-python/pull/#228))
+- Fixed example in docstring for ``ReadOnlyScoped``. ([#219](https://github.com/googleapis/google-auth-library-python/pull/#219))
+- Made ``transport.requests`` use timeouts and retries to improve reliability. ([#220](https://github.com/googleapis/google-auth-library-python/pull/#220))
+
+## 1.2.1
+
+- Excluded compiled Python files in source distributions. ([#215](https://github.com/googleapis/google-auth-library-python/pull/#215))
+- Updated docs for creating RSASigner from string. ([#213](https://github.com/googleapis/google-auth-library-python/pull/#213))
+- Use ``six.raise_from`` wherever possible. ([#212](https://github.com/googleapis/google-auth-library-python/pull/#212))
+- Fixed a typo in a comment ``seconds`` not ``sections``. ([#210](https://github.com/googleapis/google-auth-library-python/pull/#210))
+
+## 1.2.0
+
+- Added ``google.auth.credentials.AnonymousCredentials``. ([#206](https://github.com/googleapis/google-auth-library-python/pull/#206))
+- Updated the documentation to link to the Google Cloud Platform Python setup guide ([#204](https://github.com/googleapis/google-auth-library-python/pull/#204))
+
+## 1.1.1
+
+- ``google.oauth.credentials.Credentials`` now correctly inherits from ``ReadOnlyScoped`` instead of ``Scoped``. ([#200](https://github.com/googleapis/google-auth-library-python/pull/#200))
+
+## 1.1.0
+
+- Added ``service_account.Credentials.project_id``. ([#187](https://github.com/googleapis/google-auth-library-python/pull/#187))
+- Move read-only methods of ``credentials.Scoped`` into new interface ``credentials.ReadOnlyScoped``. ([#195](https://github.com/googleapis/google-auth-library-python/pull/#195), [#196](https://github.com/googleapis/google-auth-library-python/pull/#196))
+- Make ``compute_engine.Credentials`` derive from ``ReadOnlyScoped`` instead of ``Scoped``. ([#195](https://github.com/googleapis/google-auth-library-python/pull/#195))
+- Fix App Engine's expiration calculation ([#197](https://github.com/googleapis/google-auth-library-python/pull/#197))
+- Split ``crypt`` module into a package to allow alternative implementations. ([#189](https://github.com/googleapis/google-auth-library-python/pull/#189))
+- Add error message to handle case of empty string or missing file for `GOOGLE_APPLICATION_CREDENTIALS` ([#188](https://github.com/googleapis/google-auth-library-python/pull/#188))
+
+## 1.0.2
+
+- Fixed a bug where the Cloud SDK executable could not be found on Windows, leading to project ID detection failing. ([#179](https://github.com/googleapis/google-auth-library-python/pull/#179))
+- Fixed a bug where the timeout argument wasn't being passed through the httplib transport correctly. ([#175](https://github.com/googleapis/google-auth-library-python/pull/#175))
+- Added documentation for using the library on Google App Engine standard. ([#172](https://github.com/googleapis/google-auth-library-python/pull/#172))
+- Testing style updates. ([#168](https://github.com/googleapis/google-auth-library-python/pull/#168))
+- Added documentation around the oauth2client deprecation. ([#165](https://github.com/googleapis/google-auth-library-python/pull/#165))
+- Fixed a few lint issues caught by newer versions of pylint. ([#166](https://github.com/googleapis/google-auth-library-python/pull/#166))
+
+## 1.0.1
+
+- Fixed a bug in the clock skew accommodation logic where expired credentials could be used for up to 5 minutes. ([#158](https://github.com/googleapis/google-auth-library-python/pull/158))
+
+## 1.0.0
+
+Milestone release for v1.0.0.
+No significant changes since v0.10.0
+
+## 0.10.0
+
+- Added ``jwt.OnDemandCredentials``. ([#142](https://github.com/googleapis/google-auth-library-python/pull/142))
+- Added new public property ``id_token`` to ``oauth2.credentials.Credentials``. ([#150](https://github.com/googleapis/google-auth-library-python/pull/150))
+- Added the ability to set the address used to communicate with the Compute Engine metadata server via the ``GCE_METADATA_ROOT`` and ``GCE_METADATA_IP`` environment variables. ([#148](https://github.com/googleapis/google-auth-library-python/pull/148))
+- Changed the way cloud project IDs are ascertained from the Google Cloud SDK. ([#147](https://github.com/googleapis/google-auth-library-python/pull/147))
+- Modified expiration logic to add a 5 minute clock skew accommodation. ([#145](https://github.com/googleapis/google-auth-library-python/pull/145))
+
+## 0.9.0
+
+- Added ``service_account.Credentials.with_claims``. ([#140](https://github.com/googleapis/google-auth-library-python/pull/140))
+- Moved ``google.auth.oauthlib`` and ``google.auth.flow`` to a new separate package ``google_auth_oauthlib``. ([#137](https://github.com/googleapis/google-auth-library-python/pull/137), [#139](https://github.com/googleapis/google-auth-library-python/pull/139), [#135](https://github.com/googleapis/google-auth-library-python/pull/135), [#126](https://github.com/googleapis/google-auth-library-python/pull/126))
+- Added ``InstalledAppFlow`` to ``google_auth_oauthlib``. ([#128](https://github.com/googleapis/google-auth-library-python/pull/128))
+- Fixed some packaging and documentation issues. ([#131](https://github.com/googleapis/google-auth-library-python/pull/131))
+- Added a helpful error message when importing optional dependencies. ([#125](https://github.com/googleapis/google-auth-library-python/pull/125))
+- Made all properties required to reconstruct ``google.oauth2.credentials.Credentials`` public. ([#124](https://github.com/googleapis/google-auth-library-python/pull/124))
+- Added official Python 3.6 support. ([#102](https://github.com/googleapis/google-auth-library-python/pull/102))
+- Added ``jwt.Credentials.from_signing_credentials`` and removed ``service_account.Credentials.to_jwt_credentials``. ([#120](https://github.com/googleapis/google-auth-library-python/pull/120))
+
+## 0.8.0
+
+- Removed one-time token behavior from ``jwt.Credentials``, audience claim is now required and fixed. ([#117](https://github.com/googleapis/google-auth-library-python/pull/117))
+- ``crypt.Signer`` and ``crypt.Verifier`` are now abstract base classes. The concrete implementations have been renamed to ``crypt.RSASigner`` and ``crypt.RSAVerifier``. ``app_engine.Signer`` and ``iam.Signer`` now inherit from ``crypt.Signer``. ([#115](https://github.com/googleapis/google-auth-library-python/pull/115))
+- ``transport.grpc`` now correctly calls ``Credentials.before_request``. ([#116](https://github.com/googleapis/google-auth-library-python/pull/116))
+
+## 0.7.0
+
+- Added ``google.auth.iam.Signer``. ([#108](https://github.com/googleapis/google-auth-library-python/pull/108))
+- Fixed issue where ``google.auth.app_engine.Signer`` erroneously returns a tuple from ``sign()``. ([#109](https://github.com/googleapis/google-auth-library-python/pull/109))
+- Added public property ``google.auth.credentials.Signing.signer``. ([#110](https://github.com/googleapis/google-auth-library-python/pull/110))
+
+## 0.6.0
+
+- Added experimental integration with ``requests-oauthlib`` in ``google.oauth2.oauthlib`` and ``google.oauth2.flow``. ([#100](https://github.com/googleapis/google-auth-library-python/pull/100), [#105](https://github.com/googleapis/google-auth-library-python/pull/105), [#106](https://github.com/googleapis/google-auth-library-python/pull/106))
+- Fixed typo in ``google_auth_httplib2``'s README. ([#105](https://github.com/googleapis/google-auth-library-python/pull/105))
+
+## 0.5.0
+
+- Added ``app_engine.Signer``. ([#97](https://github.com/googleapis/google-auth-library-python/pull/97))
+- Added ``crypt.Signer.from_service_account_file``. ([#95](https://github.com/googleapis/google-auth-library-python/pull/95))
+- Fixed error handling in the oauth2 client. ([#96](https://github.com/googleapis/google-auth-library-python/pull/96))
+- Fixed the App Engine system tests.
+
+## 0.4.0
+
+- ``transports.grpc.secure_authorized_channel`` now passes ``kwargs`` to ``grpc.secure_channel``. ([#90](https://github.com/googleapis/google-auth-library-python/pull/90))
+- Added new property ``credentials.Singing.signer_email`` which can be used to identify the signer of a message. ([#89](https://github.com/googleapis/google-auth-library-python/pull/89))
+- (google_auth_httplib2) Added a proxy to ``httplib2.Http.connections``.
+
+## 0.3.2
+
+- Fixed an issue where an ``ImportError`` would occur if ``google.oauth2`` was imported before ``google.auth``. ([#88](https://github.com/googleapis/google-auth-library-python/pull/88))
+
+## 0.3.1
+
+- Fixed a bug where non-padded base64 encoded strings were not accepted. ([#87](https://github.com/googleapis/google-auth-library-python/pull/87))
+- Fixed a bug where ID token verification did not correctly call the HTTP request function. ([#87](https://github.com/googleapis/google-auth-library-python/pull/87))
+
+## 0.3.0
+
+- Added Google ID token verification helpers. ([#82](https://github.com/googleapis/google-auth-library-python/pull/82))
+- Swapped the ``target`` and ``request`` argument order for ``grpc.secure_authorized_channel``. ([#81](https://github.com/googleapis/google-auth-library-python/pull/81))
+- Added a user's guide. ([#79](https://github.com/googleapis/google-auth-library-python/pull/79))
+- Made ``service_account_email`` a public property on several credential classes. ([#76](https://github.com/googleapis/google-auth-library-python/pull/76))
+- Added a ``scope`` argument to ``google.auth.default``. ([#75](https://github.com/googleapis/google-auth-library-python/pull/75))
+- Added support for the ``GCLOUD_PROJECT`` environment variable. ([#73](https://github.com/googleapis/google-auth-library-python/pull/73))
+
+## 0.2.0
+
+- Added gRPC support. ([#67](https://github.com/googleapis/google-auth-library-python/pull/67))
+- Added Requests support. ([#66](https://github.com/googleapis/google-auth-library-python/pull/66))
+- Added ``google.auth.credentials.with_scopes_if_required`` helper. ([#65](https://github.com/googleapis/google-auth-library-python/pull/65))
+- Added private helper for oauth2client migration. ([#70](https://github.com/googleapis/google-auth-library-python/pull/70))
+
+## 0.1.0
+
+First release with core functionality available. This version is ready for
+initial usage and testing.
+
+- Added ``google.auth.credentials``, public interfaces for Credential types. ([#8](https://github.com/googleapis/google-auth-library-python/pull/8))
+- Added ``google.oauth2.credentials``, credentials that use OAuth 2.0 access and refresh tokens ([#24](https://github.com/googleapis/google-auth-library-python/pull/24))
+- Added ``google.oauth2.service_account``, credentials that use Service Account private keys to obtain OAuth 2.0 access tokens. ([#25](https://github.com/googleapis/google-auth-library-python/pull/25))
+- Added ``google.auth.compute_engine``, credentials that use the Compute Engine metadata service to obtain OAuth 2.0 access tokens. ([#22](https://github.com/googleapis/google-auth-library-python/pull/22))
+- Added ``google.auth.jwt.Credentials``, credentials that use a JWT as a bearer token.
+- Added ``google.auth.app_engine``, credentials that use the Google App Engine App Identity service to obtain OAuth 2.0 access tokens. ([#46](https://github.com/googleapis/google-auth-library-python/pull/46))
+- Added ``google.auth.default()``, an implementation of Google Application Default Credentials that supports automatic Project ID detection. ([#32](https://github.com/googleapis/google-auth-library-python/pull/32))
+- Added system tests for all credential types. ([#51](https://github.com/googleapis/google-auth-library-python/pull/51), [#54](https://github.com/googleapis/google-auth-library-python/pull/54), [#56](https://github.com/googleapis/google-auth-library-python/pull/56), [#58](https://github.com/googleapis/google-auth-library-python/pull/58), [#59](https://github.com/googleapis/google-auth-library-python/pull/59), [#60](https://github.com/googleapis/google-auth-library-python/pull/60), [#61](https://github.com/googleapis/google-auth-library-python/pull/61), [#62](https://github.com/googleapis/google-auth-library-python/pull/62))
+- Added ``google.auth.transports.urllib3.AuthorizedHttp``, an HTTP client that includes authentication provided by credentials. ([#19](https://github.com/googleapis/google-auth-library-python/pull/19))
+- Documentation style and formatting updates.
+
+## 0.0.1
+
+Initial release with foundational functionality for cryptography and JWTs.
+
+- ``google.auth.crypt`` for creating and verifying cryptographic signatures.
+- ``google.auth.jwt`` for creating (encoding) and verifying (decoding) JSON Web tokens.
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..46b2a08
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,43 @@
+# Contributor Code of Conduct
+
+As contributors and maintainers of this project,
+and in the interest of fostering an open and welcoming community,
+we pledge to respect all people who contribute through reporting issues,
+posting feature requests, updating documentation,
+submitting pull requests or patches, and other activities.
+
+We are committed to making participation in this project
+a harassment-free experience for everyone,
+regardless of level of experience, gender, gender identity and expression,
+sexual orientation, disability, personal appearance,
+body size, race, ethnicity, age, religion, or nationality.
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery
+* Personal attacks
+* Trolling or insulting/derogatory comments
+* Public or private harassment
+* Publishing other's private information,
+such as physical or electronic
+addresses, without explicit permission
+* Other unethical or unprofessional conduct.
+
+Project maintainers have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions
+that are not aligned to this Code of Conduct.
+By adopting this Code of Conduct,
+project maintainers commit themselves to fairly and consistently
+applying these principles to every aspect of managing this project.
+Project maintainers who do not follow or enforce the Code of Conduct
+may be permanently removed from the project team.
+
+This code of conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community.
+
+Instances of abusive, harassing, or otherwise unacceptable behavior
+may be reported by opening an issue
+or contacting one or more of the project maintainers.
+
+This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0,
+available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/)
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
new file mode 100644
index 0000000..255f33c
--- /dev/null
+++ b/CONTRIBUTING.rst
@@ -0,0 +1,194 @@
+Contributing
+============
+
+#. **Please sign one of the contributor license agreements below.**
+#. Fork the repo, develop and test your code changes, add docs.
+#. Make sure that your commit messages clearly describe the changes.
+#. Send a pull request.
+
+Here are some guidelines for hacking on ``google-auth-library-python``.
+
+Making changes
+--------------
+
+A few notes on making changes to ``google-auth-library-python``.
+
+- If you've added a new feature or modified an existing feature, be sure to
+  add or update any applicable documentation in docstrings and in the
+  documentation (in ``docs/``). You can re-generate the reference documentation
+  using ``nox -s docgen``.
+
+- The change must work fully on the following CPython versions:
+  3.6, 3.7, 3.8, 3.9, 3.10 across macOS, Linux, and Windows.
+
+- The codebase *must* have 100% test statement coverage after each commit.
+  You can test coverage via ``nox -e cover``.
+
+Testing changes
+---------------
+
+To test your changes, run unit tests with ``nox``::
+
+    $ nox -s unit
+
+
+Running system tests
+--------------------
+
+You can run the system tests with ``nox``::
+
+    $ nox -f system_tests/noxfile.py
+
+To run a single session, specify it with ``nox -s``::
+
+    $ nox -f system_tests/noxfile.py -s service_account
+
+First, set the environment variable ``GOOGLE_APPLICATION_CREDENTIALS`` to a valid service account.
+See `Creating and Managing Service Account Keys`_ for how to obtain a service account.
+
+Project and Credentials Setup
+-------------------------------
+
+Enable the IAM Service Account Credentials API on the project.
+
+To run system tests locally, you will need to set up a data directory ::
+
+    $ mkdir system_tests/data
+
+Your directory should look like this. Follow the instructions below for creating each file. ::
+
+  system_tests/
+      data/
+        authorized_user.json
+        impersonated_service_account.json
+        service_account.json
+
+
+``authorized_user.json``
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+Use the `gcloud CLI`_ to get an authorized user file ::
+
+    $ gcloud auth application-default login --scopes=https://www.googleapis.com/auth/userinfo.email,https://www.googleapis.com/auth/cloud-platform,openid
+
+You will see something like::
+
+    Credentials saved to file: [/usr/local/home/.config/gcloud/application_default_credentials.json]
+
+Copy the contents of the file to ``authorized_user.json``.
+
+Open the IAM page of the Google Cloud Console. Grant the user the `Service Account Token Creator Role`.
+This will allow the user to impersonate service accounts on the project.
+
+.. _gcloud CLI: https://cloud.google.com/sdk/gcloud/
+
+
+``service_account.json``
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+Follow `Creating and Managing Service Account Keys`_ to create a service account.
+
+Copy the credentials file to ``service_account.json``.
+
+Grant the account associated with ``service_account.json`` the following roles.
+
+- App Engine Admin (for App Engine tests)
+- Service Account Token Creator (for impersonated credentials and workload identity federation tests)
+- Pub/Sub Viewer (for gRPC tests)
+- Storage Object Viewer (for impersonated credentials tests)
+- DNS Viewer (for workload identity federation tests)
+- GCE Storage Bucket Admin (for downscoping tests)
+
+``impersonated_service_account.json``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Follow `Creating and Managing Service Account Keys`_ to create a service account.
+
+Copy the credentials file to ``impersonated_service_account.json``.
+
+.. _Creating and Managing Service Account Keys: https://cloud.google.com/iam/docs/creating-managing-service-account-keys
+
+``setup_external_accounts``
+~~~~~~~~~~~~~~~~
+
+In order to run the workload identity federation tests, you will need to set up
+a Workload Identity Pool, as well as attach relevant policy bindings for this
+new resource to our service account. To do this, make sure you have IAM Workload
+Identity Pool Admin and Security Admin permissions, and then run:
+
+  $ ./scripts/setup_external_accounts.sh
+
+and then use the output to replace the variables near
+the top of system_tests/system_tests_sync/test_external_accounts.py
+
+App Engine System Tests
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+To run the App Engine tests, you wil need to deploy a default App Engine service.
+If you already have a default service associated with your project, you can skip this step.
+
+Edit ``app.yaml`` so ``service`` is ``default`` instead of ``google-auth-system-tests``.
+From ``system_tests/app_engine_test_app`` run the following commands ::
+
+    $ pip install --target lib -r requirements.txt
+    $ gcloud app deploy -q app.yaml
+
+After the app is deployed, change ``service`` in ``app.yaml`` back to ``google-auth-system-tests``.
+You can now run the App Engine tests: ::
+
+    $ nox -f system_tests/noxfile.py -s app_engine
+
+Compute Engine Tests
+^^^^^^^^^^^^^^^^^^^^
+
+These tests cannot be run locally and will be skipped if they are run outside of Google Compute Engine.
+
+grpc Tests
+^^^^^^^^^^^^
+
+These tests use the Pub/Sub API. Grant the service account specified by `GOOGLE_APPLICATION_CREDENTIALS`
+permissions to list topics. The service account should have at least `roles/pubsub.viewer`.
+
+Coding Style
+------------
+
+This library is PEP8 & Pylint compliant. Our Pylint config is defined at
+``pylintrc`` for package code and ``pylintrc.tests`` for test code. Use
+``nox`` to check for non-compliant code::
+
+   $ nox -s lint
+
+Documentation Coverage and Building HTML Documentation
+------------------------------------------------------
+
+If you fix a bug, and the bug requires an API or behavior modification, all
+documentation in this package which references that API or behavior must be
+changed to reflect the bug fix, ideally in the same commit that fixes the bug
+or adds the feature.
+
+To build and review docs use  ``nox``::
+
+   $ nox -s docs
+
+The HTML version of the docs will be built in ``docs/_build/html``
+
+Versioning
+----------
+
+This library follows `Semantic Versioning`_.
+
+.. _Semantic Versioning: http://semver.org/
+
+It is currently in major version zero (``0.y.z``), which means that anything
+may change at any time and the public API should not be considered
+stable.
+
+Contributor License Agreements
+------------------------------
+
+Before we can accept your pull requests you'll need to sign a Contributor License Agreement (CLA):
+
+- **If you are an individual writing original source code** and **you own the intellectual property**, then you'll need to sign an `individual CLA <https://developers.google.com/open-source/cla/individual>`__.
+- **If you work for a company that wants to allow you to contribute your work**, then you'll need to sign a `corporate CLA <https://developers.google.com/open-source/cla/corporate>`__.
+
+You can sign these electronically (just scroll to the bottom). After that, we'll be able to accept your pull requests.
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
new file mode 100644
index 0000000..501db63
--- /dev/null
+++ b/CONTRIBUTORS.md
@@ -0,0 +1,96 @@
+# Contribors to oauth2client / google-auth
+
+## Maintainers
+
+* [Jon Wayne Parrott](https://github.com/jonparrott)
+* [Danny Hermes](https://github.com/dhermes)
+* [Brian Watson](https://github.com/bjwatson)
+
+Previous maintainers:
+
+* [Nathaniel Manista](https://github.com/nathanielmanistaatgoogle)
+* [Craig Citro](https://github.com/craigcitro)
+* [Joe Gregorio](https://github.com/jcgregorio)
+
+## Contributors
+
+This list is generated from git commit authors.
+
+* aalexand <[email protected]>
+* Aaron <[email protected]>
+* Adam Chainz <[email protected]>
+* [email protected]
+* Alexandre Vivien <[email protected]>
+* Ali Afshar <[email protected]>
+* Andrzej Pragacz <[email protected]>
+* [email protected]
+* Ben Demaree <[email protected]>
+* Bill Prin <[email protected], [email protected]>
+* Brendan McCollam <[email protected], [email protected]>
+* Craig Citro <[email protected], [email protected]>
+* Dan Ring <[email protected]>
+* Daniel Hermes <[email protected], [email protected]>
+* Danilo Akamine <[email protected]>
+* daryl herzmann <[email protected]>
+* dlorenc <[email protected]>
+* Dominik MiedziÅ„ski <[email protected]>
+* dr. Kertész Csaba-Zoltán <[email protected]>
+* Dustin Farris <[email protected]>
+* Eddie Warner <[email protected]>
+* Edwin Amsler <[email protected]>
+* elibixby <[email protected]>
+* Emanuele Pucciarelli <[email protected]>
+* Eric Koleda <[email protected]>
+* Frederik Creemers <[email protected]>
+* Guido van Rossum <[email protected]>
+* Harsh Vardhan <[email protected]>
+* Herr Kaste <[email protected]>
+* INADA Naoki <[email protected]>
+* JacobMoshenko <[email protected]>
+* Jay Lee <[email protected]>
+* Jed Hartman <[email protected]>
+* Jeff Terrace <[email protected], [email protected]>
+* Jeffrey Sorensen <[email protected]>
+* Jeremi Joslin <[email protected]>
+* Jin Liu <[email protected]>
+* Joe Beda <[email protected]>
+* Joe Gregorio <[email protected], [email protected]>
+* Johan Euphrosine <[email protected]>
+* John Asmuth <[email protected], [email protected]>
+* John Vandenberg <[email protected]>
+* Jon Wayne Parrott <[email protected], [email protected]>
+* Jose Alcerreca <[email protected]>
+* KCs <[email protected]>
+* Keith Maxwell <[email protected]>
+* Ken Payson <[email protected]>
+* Kevin Regan <[email protected]>
+* lraccomando <[email protected]>
+* Luar Roji <[email protected]>
+* Luke Blanshard <[email protected]>
+* Marc Cohen <[email protected]>
+* Mark Pellegrini <[email protected]>
+* Martin Trigaux <[email protected]>
+* Matt McDonald <[email protected]>
+* Nathan Naze <[email protected]>
+* Nathaniel Manista <[email protected]>
+* Orest Bolohan <[email protected]>
+* Pat Ferate <[email protected]>
+* Patrick Costello <[email protected]>
+* Rafe Kaplan <[email protected]>
+* [email protected] <[email protected]>
+* RM Saksida <[email protected]>
+* Robert Kaplow <[email protected]>
+* Robert Spies <[email protected]>
+* Sergei Trofimovich <[email protected]>
+* [email protected] <[email protected]>
+* Simon Cadman <[email protected]>
+* soltanmm <[email protected]>
+* Sébastien de Melo <[email protected]>
+* takuya sato <[email protected]>
+* thobrla <[email protected]>
+* Tom Miller <[email protected]>
+* Tony Aiuto <[email protected]>
+* Travis Hobrla <[email protected]>
+* Veres Lajos <[email protected]>
+* Vivek Seth <[email protected]>
+* Éamonn McManus <[email protected]>
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..2c28207
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,3 @@
+include README.rst LICENSE CHANGELOG.rst
+recursive-include tests *
+global-exclude *.pyc __pycache__
diff --git a/METADATA b/METADATA
new file mode 100644
index 0000000..7d78f86
--- /dev/null
+++ b/METADATA
@@ -0,0 +1,18 @@
+name: "google-auth-library-python"
+description:
+    "This library simplifies using Google’s various server-to-server "
+    "authentication mechanisms to access Google APIs."
+
+third_party {
+  url {
+    type: HOMEPAGE
+    value: "https://pypi.org/project/google-auth/"
+  }
+  url {
+    type: GIT
+    value: "https://github.com/googleapis/google-auth-library-python"
+  }
+  version: "v2.3.3"
+  last_upgrade_date { year: 2022 month: 1 day: 4 }
+  license_type: NOTICE
+}
diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MODULE_LICENSE_APACHE2
diff --git a/NOTICE b/NOTICE
new file mode 120000
index 0000000..7a694c9
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1 @@
+LICENSE
\ No newline at end of file
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..6e67161
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,67 @@
+Google Auth Python Library
+==========================
+
+|pypi|
+
+This library simplifies using Google's various server-to-server authentication
+mechanisms to access Google APIs.
+
+.. |pypi| image:: https://img.shields.io/pypi/v/google-auth.svg
+   :target: https://pypi.python.org/pypi/google-auth
+
+Installing
+----------
+
+You can install using `pip`_::
+
+    $ pip install google-auth
+
+.. _pip: https://pip.pypa.io/en/stable/
+
+For more information on setting up your Python development environment, please refer to `Python Development Environment Setup Guide`_ for Google Cloud Platform.
+
+.. _`Python Development Environment Setup Guide`: https://cloud.google.com/python/setup
+
+Supported Python Versions
+^^^^^^^^^^^^^^^^^^^^^^^^^
+Python >= 3.6
+
+Unsupported Python Versions
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+- Python == 2.7:  The last version of this library with support for Python 2.7
+  was `google.auth == 1.34.0`.
+
+- Python 3.5:   The last version of this library with support for Python 3.5
+  was `google.auth == 1.23.0`.
+
+Documentation
+-------------
+
+Google Auth Python Library has usage and reference documentation at https://googleapis.dev/python/google-auth/latest/index.html.
+
+Current Maintainers
+-------------------
+- `@busunkim96 <https://github.com/busunkim96>`_ (Bu Sun Kim)
+
+Authors
+-------
+
+- `@theacodes <https://github.com/theacodes>`_ (Thea Flowers)
+- `@dhermes <https://github.com/dhermes>`_ (Danny Hermes)
+- `@lukesneeringer <https://github.com/lukesneeringer>`_ (Luke Sneeringer)
+
+Contributing
+------------
+
+Contributions to this library are always welcome and highly encouraged.
+
+See `CONTRIBUTING.rst`_ for more information on how to get started.
+
+.. _CONTRIBUTING.rst: https://github.com/googleapis/google-auth-library-python/blob/main/CONTRIBUTING.rst
+
+License
+-------
+
+Apache 2.0 - See `the LICENSE`_ for more information.
+
+.. _the LICENSE: https://github.com/googleapis/google-auth-library-python/blob/main/LICENSE
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..8b58ae9
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,7 @@
+# Security Policy
+
+To report a security issue, please use [g.co/vulnz](https://g.co/vulnz).
+
+The Google Security Team will respond within 5 working days of your report on g.co/vulnz.
+
+We use g.co/vulnz for our intake, and do coordination and disclosure here using GitHub Security Advisory to privately discuss and fix the issue.
diff --git a/docs/_static/custom.css b/docs/_static/custom.css
new file mode 100644
index 0000000..3d0319d
--- /dev/null
+++ b/docs/_static/custom.css
@@ -0,0 +1,16 @@
+@import url('https://fonts.googleapis.com/css?family=Roboto|Roboto+Mono');
+
+@media screen and (min-width: 1080px) {
+    div.document {
+        width: 1040px;
+    }
+}
+
+code.descname {
+    color: #4885ed;
+}
+
+th.field-name {
+    min-width: 100px;
+    color: #3cba54;
+}
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..58e5b9a
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,372 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# google-auth documentation build configuration file, created by
+# sphinx-quickstart on Thu Sep 22 12:50:15 2016.
+#
+# This file is execfile()d with the current directory set to its
+# containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import pkg_resources
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+# import os
+# import sys
+# sys.path.insert(0, os.path.abspath('.'))
+
+# -- General configuration ------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#
+# needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+    "sphinx.ext.autodoc",
+    "sphinx.ext.intersphinx",
+    "sphinx.ext.viewcode",
+    "sphinx.ext.napoleon",
+    "sphinx_docstring_typing",
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ["_templates"]
+
+# The suffix(es) of source filenames.
+# You can specify multiple suffix as a list of string:
+#
+# source_suffix = ['.rst', '.md']
+source_suffix = ".rst"
+
+# The encoding of source files.
+#
+# source_encoding = 'utf-8-sig'
+
+# The root toctree document.
+root_doc = "index"
+
+# General information about the project.
+project = "google-auth"
+copyright = "2016, Google, Inc."
+author = "Google, Inc."
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = pkg_resources.get_distribution("google-auth").version
+# The full version, including alpha/beta/rc tags.
+release = version
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#
+# This is also used if you do content translation via gettext catalogs.
+# Usually you set "language" from the command line for these cases.
+language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#
+# today = ''
+#
+# Else, today_fmt is used as the format for a strftime call.
+#
+# today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This patterns also effect to html_static_path and html_extra_path
+exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
+
+# The reST default role (used for this markup: `text`) to use for all
+# documents.
+#
+# default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#
+# add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#
+add_module_names = False
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#
+# show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = "sphinx"
+
+# A list of ignored prefixes for module index sorting.
+# modindex_common_prefix = []
+
+# If true, keep warnings as "system message" paragraphs in the built documents.
+# keep_warnings = False
+
+# If true, `todo` and `todoList` produce output, else they produce nothing.
+todo_include_todos = False
+
+
+# -- Options for HTML output ----------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  See the documentation for
+# a list of builtin themes.
+#
+html_theme = "alabaster"
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further.  For a list of options available for each theme, see the
+# documentation.
+#
+html_theme_options = {
+    "description": "Google Auth Library for Python",
+    "github_user": "GoogleCloudPlatform",
+    "github_repo": "google-auth-library-python",
+    "github_banner": True,
+    "travis_button": True,
+    "font_family": "'Roboto', Georgia, sans",
+    "head_font_family": "'Roboto', Georgia, serif",
+    "code_font_family": "'Roboto Mono', 'Consolas', monospace",
+}
+
+# Add any paths that contain custom themes here, relative to this directory.
+# html_theme_path = []
+
+# The name for this set of Sphinx documents.
+# "<project> v<release> documentation" by default.
+#
+# html_title = 'google-auth v0.0.1a'
+
+# A shorter title for the navigation bar.  Default is the same as html_title.
+#
+# html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#
+# html_logo = None
+
+# The name of an image file (relative to this directory) to use as a favicon of
+# the docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#
+# html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ["_static"]
+
+# Add any extra paths that contain custom files (such as robots.txt or
+# .htaccess) here, relative to this directory. These files are copied
+# directly to the root of the documentation.
+#
+# html_extra_path = []
+
+# If not None, a 'Last updated on:' timestamp is inserted at every page
+# bottom, using the given strftime format.
+# The empty string is equivalent to '%b %d, %Y'.
+#
+# html_last_updated_fmt = None
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#
+# html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#
+
+html_sidebars = {
+    "**": ["about.html", "navigation.html", "relations.html", "searchbox.html"]
+}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#
+# html_additional_pages = {}
+
+# If false, no module index is generated.
+#
+# html_domain_indices = True
+
+# If false, no index is generated.
+#
+# html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#
+# html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#
+# html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#
+# html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#
+# html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it.  The value of this option must be the
+# base URL from which the finished HTML is served.
+#
+# html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+# html_file_suffix = None
+
+# Language to be used for generating the HTML full-text search index.
+# Sphinx supports the following languages:
+#   'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja'
+#   'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh'
+#
+# html_search_language = 'en'
+
+# A dictionary with options for the search language support, empty by default.
+# 'ja' uses this config value.
+# 'zh' user can custom change `jieba` dictionary path.
+#
+# html_search_options = {'type': 'default'}
+
+# The name of a javascript file (relative to the configuration directory) that
+# implements a search results scorer. If empty, the default will be used.
+#
+# html_search_scorer = 'scorer.js'
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = "google-authdoc"
+
+# -- Options for LaTeX output ---------------------------------------------
+
+latex_elements = {
+    # The paper size ('letterpaper' or 'a4paper').
+    #
+    # 'papersize': 'letterpaper',
+    # The font size ('10pt', '11pt' or '12pt').
+    #
+    # 'pointsize': '10pt',
+    # Additional stuff for the LaTeX preamble.
+    #
+    # 'preamble': '',
+    # Latex figure (float) alignment
+    #
+    # 'figure_align': 'htbp',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+#  author, documentclass [howto, manual, or own class]).
+latex_documents = [
+    (root_doc, "google-auth.tex", "google-auth Documentation", "Google, Inc.", "manual")
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#
+# latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#
+# latex_use_parts = False
+
+# If true, show page references after internal links.
+#
+# latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#
+# latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+#
+# latex_appendices = []
+
+# It false, will not define \strong, \code, 	itleref, \crossref ... but only
+# \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added
+# packages.
+#
+# latex_keep_old_macro_names = True
+
+# If false, no module index is generated.
+#
+# latex_domain_indices = True
+
+
+# -- Options for manual page output ---------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [(root_doc, "google-auth", "google-auth Documentation", [author], 1)]
+
+# If true, show URL addresses after external links.
+#
+# man_show_urls = False
+
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+#  dir menu entry, description, category)
+texinfo_documents = [
+    (
+        root_doc,
+        "google-auth",
+        "google-auth Documentation",
+        author,
+        "google-auth",
+        "One line description of project.",
+        "Miscellaneous",
+    )
+]
+
+# Documents to append as an appendix to all manuals.
+#
+# texinfo_appendices = []
+
+# If false, no module index is generated.
+#
+# texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#
+# texinfo_show_urls = 'footnote'
+
+# If true, do not generate a @detailmenu in the "Top" node's menu.
+#
+# texinfo_no_detailmenu = False
+
+
+# Example configuration for intersphinx: refer to the Python standard library.
+intersphinx_mapping = {
+    "python": ("https://docs.python.org/3.5", None),
+    "urllib3": ("https://urllib3.readthedocs.io/en/stable", None),
+    "requests": ("https://requests.kennethreitz.org/en/master/", None),
+    "requests-oauthlib": ("https://requests-oauthlib.readthedocs.io/en/stable/", None),
+}
+
+# Autodoc config
+autoclass_content = "both"
+autodoc_member_order = "bysource"
+autodoc_mock_imports = ["grpc"]
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 0000000..8a5f13a
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,74 @@
+google-auth
+===========
+
+.. toctree::
+   :hidden:
+   :maxdepth: 2
+
+   user-guide
+   Reference <reference/modules>
+
+google-auth is the Google authentication library for Python. This library
+provides the ability to authenticate to Google APIs using various methods. It
+also provides integration with several HTTP libraries.
+
+- Support for Google :func:`Application Default Credentials <google.auth.default>`.
+- Support for signing and verifying :mod:`JWTs <google.auth.jwt>`.
+- Support for creating `Google ID Tokens <user-guide.html#identity-tokens>`__.
+- Support for verifying and decoding :mod:`ID Tokens <google.oauth2.id_token>`.
+- Support for Google :mod:`Service Account credentials <google.oauth2.service_account>`.
+- Support for Google :mod:`Impersonated Credentials <google.auth.impersonated_credentials>`.
+- Support for :mod:`Google Compute Engine credentials <google.auth.compute_engine>`.
+- Support for :mod:`Google App Engine standard credentials <google.auth.app_engine>`.
+- Support for :mod:`Identity Pool credentials <google.auth.identity_pool>`.
+- Support for :mod:`AWS credentials <google.auth.aws>`.
+- Support for :mod:`Downscoping with Credential Access Boundaries credentials <google.auth.downscoped>`.
+- Support for various transports, including
+  :mod:`Requests <google.auth.transport.requests>`,
+  :mod:`urllib3 <google.auth.transport.urllib3>`, and
+  :mod:`gRPC <google.auth.transport.grpc>`.
+
+.. note:: ``oauth2client`` was recently deprecated in favor of this library. For more details on the deprecation, see :doc:`oauth2client-deprecation`.
+
+Installing
+----------
+
+google-auth can be installed with `pip`_::
+
+    $ pip install --upgrade google-auth
+
+google-auth is open-source, so you can alternatively grab the source code from
+`GitHub`_ and install from source.
+
+
+For more information on setting up your Python development environment, please refer to `Python Development Environment Setup Guide`_ for Google Cloud Platform.
+
+.. _`Python Development Environment Setup Guide`: https://cloud.google.com/python/setup
+.. _pip: https://pip.pypa.io
+.. _GitHub: https://github.com/GoogleCloudPlatform/google-auth-library-python
+
+Usage
+-----
+
+The :doc:`user-guide` is the place to go to learn how to use the library and
+accomplish common tasks.
+
+The :doc:`Module Reference <reference/modules>` documentation provides API-level documentation.
+
+License
+-------
+
+google-auth is made available under the Apache License, Version 2.0. For more
+details, see `LICENSE`_
+
+.. _LICENSE:
+    https://github.com/GoogleCloudPlatform/google-auth-library-python/blob/main/LICENSE
+
+Contributing
+------------
+
+We happily welcome contributions, please see our `contributing`_ documentation
+for details.
+
+.. _contributing:
+    https://github.com/GoogleCloudPlatform/google-auth-library-python/blob/main/CONTRIBUTING.rst
diff --git a/docs/oauth2client-deprecation.rst b/docs/oauth2client-deprecation.rst
new file mode 100644
index 0000000..2802c3e
--- /dev/null
+++ b/docs/oauth2client-deprecation.rst
@@ -0,0 +1,117 @@
+:orphan:
+
+oauth2client deprecation
+========================
+
+This page is intended for existing users of the `oauth2client`_ who want to
+understand the reasons for its deprecation and how this library relates to
+``oauth2client``.
+
+.. _oauth2client: https://github.com/google/oauth2client
+
+Reasons for deprecation
+-----------------------
+
+#. Lack of ownership: ``oauth2client`` has lacked a definitive owner since
+   around 2013.
+#. Fragile and ad-hoc design: ``oauth2client`` is the result of several years
+   of ad-hoc additions and organic, uncontrolled growth. This has led to a
+   library that lacks overall design and cohesion. The convoluted class
+   hierarchy is a symptom of this.
+#. Lack of a secure, thread-safe, and modern transport: ``oauth2client`` is
+   inextricably dependent on `httplib2`_. ``httplib2`` is largely unmaintained
+   (although recently there are a small group of volunteers attempting to
+   maintain it).
+#. Lack of clear purpose and goals: The library is named "oauth2client" but is
+   actually a pretty poor OAuth 2.0 client and does a lot of things that have
+   nothing to do with OAuth and its related RFCs.
+
+.. _httplib2: https://github.com/httplib2/httplib2
+
+We originally planned to address these issues within ``oauth2client``, however,
+we determined that the number of breaking changes needed would be absolutely
+untenable for downstream users. It would essentially involve our users having
+to rewrite significant portions of their code if they needed to upgrade (either
+directly or indirectly through a dependency). Instead, we've chosen to create a
+new replacement library that can live side-by-side with ``oauth2client`` and
+allow users to migrate gradually. We believe that this was the least painful
+option.
+
+Replacement
+-----------
+
+The long-term replacement for ``oauth2client`` is this library,
+``google-auth``. This library addresses the major issues with oauthclient:
+
+#. Clear ownership: google-auth is owned by the teams that maintain the
+   `Cloud Client Libraries`_, `gRPC`_, and the
+   `Code Samples for Google Cloud Platform`_.
+#. Thought-out design: using the lessons learned from ``oauth2client``, we have
+   designed a better module and class hierarchy. The ``v1.0.0`` release of this
+   library should provide long-term API stability.
+#. Modern, secure, and extensible transports: ``google-auth`` supports
+   `urllib3`_, `requests`_, `gRPC`_, and has `legacy support for httplib2`_ to
+   help clients migration. It is transport agnostic and has explicit support
+   for adding new transports.
+#. Clear purpose and goals: ``google-auth`` is explicitly focused on
+   Google-specific authentication, especially the server-to-server (service
+   account) use case.
+ 
+Because we reduced the scope of the library, there are several features in
+``oauth2client`` we intentionally are not supporting in the ``v1.0.0`` release
+of ``google-auth``. This does not mean we are not interested in supporting
+these features, we just didn't feel they should be part of the initial API.
+As downstream users ask for these features we will determine the best way to
+serve those use cases without allowing the library to become a dumping ground.
+ 
+The unsupported features are:
+
+#. Support for obtaining user credentials. While this library has support for
+   using user credentials, there are no provisions in the core library for
+   doing the three-party OAuth 2.0 flow to obtain authorization from a user.
+   Instead, we are opting to provide a separate package that does integration
+   with `oauthlib`_, `google-auth-oauthlib`_. When that library has a stable
+   API, we will consider its inclusion into the core library.
+#. Support for storing credentials. The only credentials type that needs to
+   be stored are user credentials. We have a `discussion thread`_ on what level
+   of support we should do. It's very likely we'll choose to provide an
+   abstract interface for this and leave it up to application to provide
+   storage implementation specific to their use case. It's also very likely
+   that we will also incubate this functionality in the
+   `google-auth-oauthlib`_ library before including it in the core library.
+
+.. _Cloud Client Libraries: https://github.com/googlecloudplatform/google-cloud-python
+.. _gRPC: http://www.grpc.io/
+.. _Code Samples for Google Cloud Platform: https://github.com/googlecloudplatform/python-docs-samples
+.. _urllib3: https://urllib3.readthedocs.io
+.. _requests: http://python-requests.org
+.. _legacy support for httplib2: https://pypi.python.org/pypi/google-auth-httplib2
+.. _oauthlib: https://oauthlib.readthedocs.io
+.. _google-auth-oauthlib: http://google-auth-oauthlib.readthedocs.io/
+.. _discussion thread: https://github.com/GoogleCloudPlatform/google-auth-library-python/issues/33
+
+
+Post-deprecation support
+------------------------
+
+While ``oauth2client`` will not be implementing or accepting any new features,
+the ``google-auth`` team will continue to accept bug reports and fix critical
+bugs. We will make patch releases as necessary. We have no plans to remove the
+library from GitHub or PyPI. Also, we have made sure that the
+`google-api-python-client`_ library supports oauth2client and google-auth and
+will continue to do so indefinitely.
+
+It is important to note that we will not be adding any features, even if an
+external user goes through the trouble of sending a pull request. This policy
+is in place because without it we will perpetuate the circumstances that led
+to ``oauth2client`` being in the semi-unmaintained state it was in previously.
+
+Some old documentation and examples may use ``oauth2client`` instead of
+``google-auth``. We are working to update all of these but it does take a
+significant amount of time. Since we are still iterating on user auth, some
+samples that use user auth will not be updated until we have settled on a final
+interface. If you find any samples you feel should be updated, please
+`file a bug`_.
+
+.. _google-api-python-client: https://github.com/google/google-api-python-client
+.. _file a bug: https://github.com/GoogleCloudPlatform/google-auth-library-python/issues
diff --git a/docs/reference/google.auth._credentials_async.rst b/docs/reference/google.auth._credentials_async.rst
new file mode 100644
index 0000000..683139a
--- /dev/null
+++ b/docs/reference/google.auth._credentials_async.rst
@@ -0,0 +1,7 @@
+google.auth.credentials\_async module
+=====================================
+
+.. automodule:: google.auth._credentials_async
+   :members:
+   :inherited-members:
+   :show-inheritance:
diff --git a/docs/reference/google.auth._jwt_async.rst b/docs/reference/google.auth._jwt_async.rst
new file mode 100644
index 0000000..d27984b
--- /dev/null
+++ b/docs/reference/google.auth._jwt_async.rst
@@ -0,0 +1,7 @@
+google.auth.jwt\_async module
+=============================
+
+.. automodule:: google.auth._jwt_async
+   :members:
+   :inherited-members:
+   :show-inheritance:
diff --git a/docs/reference/google.auth.app_engine.rst b/docs/reference/google.auth.app_engine.rst
new file mode 100644
index 0000000..2142b6f
--- /dev/null
+++ b/docs/reference/google.auth.app_engine.rst
@@ -0,0 +1,7 @@
+google.auth.app\_engine module
+==============================
+
+.. automodule:: google.auth.app_engine
+   :members:
+   :inherited-members:
+   :show-inheritance:
diff --git a/docs/reference/google.auth.aws.rst b/docs/reference/google.auth.aws.rst
new file mode 100644
index 0000000..9c3966b
--- /dev/null
+++ b/docs/reference/google.auth.aws.rst
@@ -0,0 +1,7 @@
+google.auth.aws module
+======================
+
+.. automodule:: google.auth.aws
+   :members:
+   :inherited-members:
+   :show-inheritance:
diff --git a/docs/reference/google.auth.compute_engine.credentials.rst b/docs/reference/google.auth.compute_engine.credentials.rst
new file mode 100644
index 0000000..782d95f
--- /dev/null
+++ b/docs/reference/google.auth.compute_engine.credentials.rst
@@ -0,0 +1,7 @@
+google.auth.compute\_engine.credentials module
+==============================================
+
+.. automodule:: google.auth.compute_engine.credentials
+   :members:
+   :inherited-members:
+   :show-inheritance:
diff --git a/docs/reference/google.auth.compute_engine.rst b/docs/reference/google.auth.compute_engine.rst
new file mode 100644
index 0000000..819248c
--- /dev/null
+++ b/docs/reference/google.auth.compute_engine.rst
@@ -0,0 +1,15 @@
+google.auth.compute\_engine package
+===================================
+
+.. automodule:: google.auth.compute_engine
+   :members:
+   :inherited-members:
+   :show-inheritance:
+
+Submodules
+----------
+
+.. toctree::
+   :maxdepth: 4
+
+   google.auth.compute_engine.credentials
diff --git a/docs/reference/google.auth.credentials.rst b/docs/reference/google.auth.credentials.rst
new file mode 100644
index 0000000..18d1d8c
--- /dev/null
+++ b/docs/reference/google.auth.credentials.rst
@@ -0,0 +1,7 @@
+google.auth.credentials module
+==============================
+
+.. automodule:: google.auth.credentials
+   :members:
+   :inherited-members:
+   :show-inheritance:
diff --git a/docs/reference/google.auth.crypt.base.rst b/docs/reference/google.auth.crypt.base.rst
new file mode 100644
index 0000000..a899650
--- /dev/null
+++ b/docs/reference/google.auth.crypt.base.rst
@@ -0,0 +1,7 @@
+google.auth.crypt.base module
+=============================
+
+.. automodule:: google.auth.crypt.base
+   :members:
+   :inherited-members:
+   :show-inheritance:
diff --git a/docs/reference/google.auth.crypt.es256.rst b/docs/reference/google.auth.crypt.es256.rst
new file mode 100644
index 0000000..5a63184
--- /dev/null
+++ b/docs/reference/google.auth.crypt.es256.rst
@@ -0,0 +1,7 @@
+google.auth.crypt.es256 module
+==============================
+
+.. automodule:: google.auth.crypt.es256
+   :members:
+   :inherited-members:
+   :show-inheritance:
diff --git a/docs/reference/google.auth.crypt.rsa.rst b/docs/reference/google.auth.crypt.rsa.rst
new file mode 100644
index 0000000..7060b03
--- /dev/null
+++ b/docs/reference/google.auth.crypt.rsa.rst
@@ -0,0 +1,7 @@
+google.auth.crypt.rsa module
+============================
+
+.. automodule:: google.auth.crypt.rsa
+   :members:
+   :inherited-members:
+   :show-inheritance:
diff --git a/docs/reference/google.auth.crypt.rst b/docs/reference/google.auth.crypt.rst
new file mode 100644
index 0000000..ff38fa3
--- /dev/null
+++ b/docs/reference/google.auth.crypt.rst
@@ -0,0 +1,17 @@
+google.auth.crypt package
+=========================
+
+.. automodule:: google.auth.crypt
+   :members:
+   :inherited-members:
+   :show-inheritance:
+
+Submodules
+----------
+
+.. toctree::
+   :maxdepth: 4
+
+   google.auth.crypt.base
+   google.auth.crypt.es256
+   google.auth.crypt.rsa
diff --git a/docs/reference/google.auth.downscoped.rst b/docs/reference/google.auth.downscoped.rst
new file mode 100644
index 0000000..79668f9
--- /dev/null
+++ b/docs/reference/google.auth.downscoped.rst
@@ -0,0 +1,7 @@
+google.auth.downscoped module
+=============================
+
+.. automodule:: google.auth.downscoped
+   :members:
+   :inherited-members:
+   :show-inheritance:
diff --git a/docs/reference/google.auth.environment_vars.rst b/docs/reference/google.auth.environment_vars.rst
new file mode 100644
index 0000000..5996e99
--- /dev/null
+++ b/docs/reference/google.auth.environment_vars.rst
@@ -0,0 +1,7 @@
+google.auth.environment\_vars module
+====================================
+
+.. automodule:: google.auth.environment_vars
+   :members:
+   :inherited-members:
+   :show-inheritance:
diff --git a/docs/reference/google.auth.exceptions.rst b/docs/reference/google.auth.exceptions.rst
new file mode 100644
index 0000000..c87a7f2
--- /dev/null
+++ b/docs/reference/google.auth.exceptions.rst
@@ -0,0 +1,7 @@
+google.auth.exceptions module
+=============================
+
+.. automodule:: google.auth.exceptions
+   :members:
+   :inherited-members:
+   :show-inheritance:
diff --git a/docs/reference/google.auth.external_account.rst b/docs/reference/google.auth.external_account.rst
new file mode 100644
index 0000000..0681eaa
--- /dev/null
+++ b/docs/reference/google.auth.external_account.rst
@@ -0,0 +1,7 @@
+google.auth.external\_account module
+====================================
+
+.. automodule:: google.auth.external_account
+   :members:
+   :inherited-members:
+   :show-inheritance:
diff --git a/docs/reference/google.auth.iam.rst b/docs/reference/google.auth.iam.rst
new file mode 100644
index 0000000..8472ed7
--- /dev/null
+++ b/docs/reference/google.auth.iam.rst
@@ -0,0 +1,7 @@
+google.auth.iam module
+======================
+
+.. automodule:: google.auth.iam
+   :members:
+   :inherited-members:
+   :show-inheritance:
diff --git a/docs/reference/google.auth.identity_pool.rst b/docs/reference/google.auth.identity_pool.rst
new file mode 100644
index 0000000..48d9902
--- /dev/null
+++ b/docs/reference/google.auth.identity_pool.rst
@@ -0,0 +1,7 @@
+google.auth.identity\_pool module
+=================================
+
+.. automodule:: google.auth.identity_pool
+   :members:
+   :inherited-members:
+   :show-inheritance:
diff --git a/docs/reference/google.auth.impersonated_credentials.rst b/docs/reference/google.auth.impersonated_credentials.rst
new file mode 100644
index 0000000..f139ccf
--- /dev/null
+++ b/docs/reference/google.auth.impersonated_credentials.rst
@@ -0,0 +1,7 @@
+google.auth.impersonated\_credentials module
+============================================
+
+.. automodule:: google.auth.impersonated_credentials
+   :members:
+   :inherited-members:
+   :show-inheritance:
diff --git a/docs/reference/google.auth.jwt.rst b/docs/reference/google.auth.jwt.rst
new file mode 100644
index 0000000..c7c2fdf
--- /dev/null
+++ b/docs/reference/google.auth.jwt.rst
@@ -0,0 +1,7 @@
+google.auth.jwt module
+======================
+
+.. automodule:: google.auth.jwt
+   :members:
+   :inherited-members:
+   :show-inheritance:
diff --git a/docs/reference/google.auth.rst b/docs/reference/google.auth.rst
new file mode 100644
index 0000000..06cc267
--- /dev/null
+++ b/docs/reference/google.auth.rst
@@ -0,0 +1,37 @@
+google.auth package
+===================
+
+.. automodule:: google.auth
+   :members:
+   :inherited-members:
+   :show-inheritance:
+
+Subpackages
+-----------
+
+.. toctree::
+   :maxdepth: 4
+
+   google.auth.compute_engine
+   google.auth.crypt
+   google.auth.transport
+
+Submodules
+----------
+
+.. toctree::
+   :maxdepth: 4
+
+   google.auth.app_engine
+   google.auth.aws
+   google.auth.credentials
+   google.auth._credentials_async
+   google.auth.downscoped
+   google.auth.environment_vars
+   google.auth.exceptions
+   google.auth.external_account
+   google.auth.iam
+   google.auth.identity_pool
+   google.auth.impersonated_credentials
+   google.auth.jwt
+   google.auth._jwt_async
diff --git a/docs/reference/google.auth.transport._aiohttp_requests.rst b/docs/reference/google.auth.transport._aiohttp_requests.rst
new file mode 100644
index 0000000..44fc4e5
--- /dev/null
+++ b/docs/reference/google.auth.transport._aiohttp_requests.rst
@@ -0,0 +1,7 @@
+google.auth.transport.aiohttp\_requests module
+==============================================
+
+.. automodule:: google.auth.transport._aiohttp_requests
+   :members:
+   :inherited-members:
+   :show-inheritance:
diff --git a/docs/reference/google.auth.transport.grpc.rst b/docs/reference/google.auth.transport.grpc.rst
new file mode 100644
index 0000000..f9f3442
--- /dev/null
+++ b/docs/reference/google.auth.transport.grpc.rst
@@ -0,0 +1,7 @@
+google.auth.transport.grpc module
+=================================
+
+.. automodule:: google.auth.transport.grpc
+   :members:
+   :inherited-members:
+   :show-inheritance:
diff --git a/docs/reference/google.auth.transport.mtls.rst b/docs/reference/google.auth.transport.mtls.rst
new file mode 100644
index 0000000..11b50e2
--- /dev/null
+++ b/docs/reference/google.auth.transport.mtls.rst
@@ -0,0 +1,7 @@
+google.auth.transport.mtls module
+=================================
+
+.. automodule:: google.auth.transport.mtls
+   :members:
+   :inherited-members:
+   :show-inheritance:
diff --git a/docs/reference/google.auth.transport.requests.rst b/docs/reference/google.auth.transport.requests.rst
new file mode 100644
index 0000000..5f0c23c
--- /dev/null
+++ b/docs/reference/google.auth.transport.requests.rst
@@ -0,0 +1,7 @@
+google.auth.transport.requests module
+=====================================
+
+.. automodule:: google.auth.transport.requests
+   :members:
+   :inherited-members:
+   :show-inheritance:
diff --git a/docs/reference/google.auth.transport.rst b/docs/reference/google.auth.transport.rst
new file mode 100644
index 0000000..f1d1988
--- /dev/null
+++ b/docs/reference/google.auth.transport.rst
@@ -0,0 +1,19 @@
+google.auth.transport package
+=============================
+
+.. automodule:: google.auth.transport
+   :members:
+   :inherited-members:
+   :show-inheritance:
+
+Submodules
+----------
+
+.. toctree::
+   :maxdepth: 4
+
+   google.auth.transport._aiohttp_requests
+   google.auth.transport.grpc
+   google.auth.transport.mtls
+   google.auth.transport.requests
+   google.auth.transport.urllib3
diff --git a/docs/reference/google.auth.transport.urllib3.rst b/docs/reference/google.auth.transport.urllib3.rst
new file mode 100644
index 0000000..667bb09
--- /dev/null
+++ b/docs/reference/google.auth.transport.urllib3.rst
@@ -0,0 +1,7 @@
+google.auth.transport.urllib3 module
+====================================
+
+.. automodule:: google.auth.transport.urllib3
+   :members:
+   :inherited-members:
+   :show-inheritance:
diff --git a/docs/reference/google.oauth2._credentials_async.rst b/docs/reference/google.oauth2._credentials_async.rst
new file mode 100644
index 0000000..d0df1e8
--- /dev/null
+++ b/docs/reference/google.oauth2._credentials_async.rst
@@ -0,0 +1,7 @@
+google.oauth2.credentials\_async module
+=======================================
+
+.. automodule:: google.oauth2._credentials_async
+   :members:
+   :inherited-members:
+   :show-inheritance:
diff --git a/docs/reference/google.oauth2._service_account_async.rst b/docs/reference/google.oauth2._service_account_async.rst
new file mode 100644
index 0000000..8aba0d8
--- /dev/null
+++ b/docs/reference/google.oauth2._service_account_async.rst
@@ -0,0 +1,7 @@
+google.oauth2.service\_account\_async module
+============================================
+
+.. automodule:: google.oauth2._service_account_async
+   :members:
+   :inherited-members:
+   :show-inheritance:
diff --git a/docs/reference/google.oauth2.credentials.rst b/docs/reference/google.oauth2.credentials.rst
new file mode 100644
index 0000000..d3bdc16
--- /dev/null
+++ b/docs/reference/google.oauth2.credentials.rst
@@ -0,0 +1,7 @@
+google.oauth2.credentials module
+================================
+
+.. automodule:: google.oauth2.credentials
+   :members:
+   :inherited-members:
+   :show-inheritance:
diff --git a/docs/reference/google.oauth2.id_token.rst b/docs/reference/google.oauth2.id_token.rst
new file mode 100644
index 0000000..fbe6eab
--- /dev/null
+++ b/docs/reference/google.oauth2.id_token.rst
@@ -0,0 +1,7 @@
+google.oauth2.id\_token module
+==============================
+
+.. automodule:: google.oauth2.id_token
+   :members:
+   :inherited-members:
+   :show-inheritance:
diff --git a/docs/reference/google.oauth2.rst b/docs/reference/google.oauth2.rst
new file mode 100644
index 0000000..2a8a7a5
--- /dev/null
+++ b/docs/reference/google.oauth2.rst
@@ -0,0 +1,21 @@
+google.oauth2 package
+=====================
+
+.. automodule:: google.oauth2
+   :members:
+   :inherited-members:
+   :show-inheritance:
+
+Submodules
+----------
+
+.. toctree::
+   :maxdepth: 4
+
+   google.oauth2.credentials
+   google.oauth2._credentials_async
+   google.oauth2.id_token
+   google.oauth2.service_account
+   google.oauth2._service_account_async
+   google.oauth2.sts
+   google.oauth2.utils
diff --git a/docs/reference/google.oauth2.service_account.rst b/docs/reference/google.oauth2.service_account.rst
new file mode 100644
index 0000000..8d8fcd3
--- /dev/null
+++ b/docs/reference/google.oauth2.service_account.rst
@@ -0,0 +1,7 @@
+google.oauth2.service\_account module
+=====================================
+
+.. automodule:: google.oauth2.service_account
+   :members:
+   :inherited-members:
+   :show-inheritance:
diff --git a/docs/reference/google.oauth2.sts.rst b/docs/reference/google.oauth2.sts.rst
new file mode 100644
index 0000000..49d99df
--- /dev/null
+++ b/docs/reference/google.oauth2.sts.rst
@@ -0,0 +1,7 @@
+google.oauth2.sts module
+========================
+
+.. automodule:: google.oauth2.sts
+   :members:
+   :inherited-members:
+   :show-inheritance:
diff --git a/docs/reference/google.oauth2.utils.rst b/docs/reference/google.oauth2.utils.rst
new file mode 100644
index 0000000..5b039ea
--- /dev/null
+++ b/docs/reference/google.oauth2.utils.rst
@@ -0,0 +1,7 @@
+google.oauth2.utils module
+==========================
+
+.. automodule:: google.oauth2.utils
+   :members:
+   :inherited-members:
+   :show-inheritance:
diff --git a/docs/reference/google.rst b/docs/reference/google.rst
new file mode 100644
index 0000000..f122ca1
--- /dev/null
+++ b/docs/reference/google.rst
@@ -0,0 +1,16 @@
+google package
+==============
+
+.. automodule:: google
+   :members:
+   :inherited-members:
+   :show-inheritance:
+
+Subpackages
+-----------
+
+.. toctree::
+   :maxdepth: 4
+
+   google.auth
+   google.oauth2
diff --git a/docs/reference/modules.rst b/docs/reference/modules.rst
new file mode 100644
index 0000000..b1816cc
--- /dev/null
+++ b/docs/reference/modules.rst
@@ -0,0 +1,7 @@
+google
+======
+
+.. toctree::
+   :maxdepth: 4
+
+   google
diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt
new file mode 100644
index 0000000..89ad689
--- /dev/null
+++ b/docs/requirements-docs.txt
@@ -0,0 +1,5 @@
+cryptography
+sphinx-docstring-typing
+urllib3
+requests
+requests-oauthlib
diff --git a/docs/user-guide.rst b/docs/user-guide.rst
new file mode 100644
index 0000000..ccece57
--- /dev/null
+++ b/docs/user-guide.rst
@@ -0,0 +1,769 @@
+User Guide
+==========
+
+.. currentmodule:: google.auth
+
+Credentials and account types
+-----------------------------
+
+:class:`~credentials.Credentials` are the means of identifying an application or
+user to a service or API. Credentials can be obtained with three different types
+of accounts: *service accounts*, *user accounts* and *external accounts*.
+
+Credentials from service accounts identify a particular application. These types
+of credentials are used in server-to-server use cases, such as accessing a
+database. This library primarily focuses on service account credentials.
+
+Credentials from user accounts are obtained by asking the user to authorize
+access to their data. These types of credentials are used in cases where your
+application needs access to a user's data in another service, such as accessing
+a user's documents in Google Drive. This library provides no support for
+obtaining user credentials, but does provide limited support for using user
+credentials.
+
+Credentials from external accounts (workload identity federation) are used to
+identify a particular application from an on-prem or non-Google Cloud platform
+including Amazon Web Services (AWS), Microsoft Azure or any identity provider
+that supports OpenID Connect (OIDC).
+
+Obtaining credentials
+---------------------
+
+.. _application-default:
+
+Application default credentials
++++++++++++++++++++++++++++++++
+
+`Google Application Default Credentials`_ abstracts authentication across the
+different Google Cloud Platform hosting environments. When running on any Google
+Cloud hosting environment or when running locally with the `Google Cloud SDK`_
+installed, :func:`default` can automatically determine the credentials from the
+environment::
+
+    import google.auth
+
+    credentials, project = google.auth.default()
+
+If your application requires specific scopes::
+
+    credentials, project = google.auth.default(
+        scopes=['https://www.googleapis.com/auth/cloud-platform'])
+
+Application Default Credentials also support workload identity federation to
+access Google Cloud resources from non-Google Cloud platforms including Amazon
+Web Services (AWS), Microsoft Azure or any identity provider that supports
+OpenID Connect (OIDC). Workload identity federation is recommended for
+non-Google Cloud environments as it avoids the need to download, manage and
+store service account private keys locally.
+
+.. _Google Application Default Credentials:
+    https://developers.google.com/identity/protocols/
+    application-default-credentials
+.. _Google Cloud SDK: https://cloud.google.com/sdk
+
+
+Service account private key files
++++++++++++++++++++++++++++++++++
+
+A service account private key file can be used to obtain credentials for a
+service account. You can create a private key using the `Credentials page of the
+Google Cloud Console`_. Once you have a private key you can either obtain
+credentials one of three ways:
+
+1. Set the ``GOOGLE_APPLICATION_CREDENTIALS`` environment variable to the full
+   path to your service account private key file
+
+   .. code-block:: bash
+
+        $ export GOOGLE_APPLICATION_CREDENTIALS=/path/to/key.json
+
+   Then, use :ref:`application default credentials <application-default>`.
+   :func:`default` checks for the ``GOOGLE_APPLICATION_CREDENTIALS``
+   environment variable before all other checks, so this will always use the
+   credentials you explicitly specify.
+
+2. Use :meth:`service_account.Credentials.from_service_account_file
+   <google.oauth2.service_account.Credentials.from_service_account_file>`::
+
+        from google.oauth2 import service_account
+
+        credentials = service_account.Credentials.from_service_account_file(
+            '/path/to/key.json')
+
+        scoped_credentials = credentials.with_scopes(
+            ['https://www.googleapis.com/auth/cloud-platform'])
+
+3. Use :meth:`service_account.Credentials.from_service_account_info
+   <google.oauth2.service_account.Credentials.from_service_account_info>`::
+
+        import json
+
+        from google.oauth2 import service_account
+
+        json_acct_info = json.loads(function_to_get_json_creds())
+        credentials = service_account.Credentials.from_service_account_info(
+            json_acct_info)
+
+        scoped_credentials = credentials.with_scopes(
+            ['https://www.googleapis.com/auth/cloud-platform'])
+
+.. warning:: Private keys must be kept secret. If you expose your private key it
+    is recommended to revoke it immediately from the Google Cloud Console.
+
+.. _Credentials page of the Google Cloud Console:
+    https://console.cloud.google.com/apis/credentials
+
+Compute Engine, Container Engine, and the App Engine flexible environment
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+Applications running on `Compute Engine`_, `Container Engine`_, or the `App
+Engine flexible environment`_ can obtain credentials provided by `Compute
+Engine service accounts`_. When running on these platforms you can obtain
+credentials for the service account one of two ways:
+
+1. Use :ref:`application default credentials <application-default>`.
+   :func:`default` will automatically detect if these credentials are available.
+
+2. Use :class:`compute_engine.Credentials`::
+
+        from google.auth import compute_engine
+
+        credentials = compute_engine.Credentials()
+
+.. _Compute Engine: https://cloud.google.com/compute
+.. _Container Engine: https://cloud.google.com/container-engine
+.. _App Engine flexible environment:
+    https://cloud.google.com/appengine/docs/flexible/
+.. _Compute Engine service accounts:
+    https://cloud.google.com/compute/docs/access/service-accounts
+
+The App Engine standard environment
++++++++++++++++++++++++++++++++++++
+
+Applications running on the `App Engine standard environment`_ can obtain
+credentials provided by the `App Engine App Identity API`_. You can obtain
+credentials one of two ways:
+
+1. Use :ref:`application default credentials <application-default>`.
+   :func:`default` will automatically detect if these credentials are available.
+
+2. Use :class:`app_engine.Credentials`::
+
+        from google.auth import app_engine
+
+        credentials = app_engine.Credentials()
+
+In order to make authenticated requests in the App Engine environment using the
+credentials and transports provided by this library, you need to follow a few
+additional steps:
+
+#. If you are using the :mod:`google.auth.transport.requests` transport, vendor
+   in the `requests-toolbelt`_ library into your app, and enable the App Engine
+   monkeypatch. Refer `App Engine documentation`_ for more details on this.
+#. To make HTTPS calls, enable the ``ssl`` library for your app by adding the
+   following configuration to the ``app.yaml`` file::
+
+        libraries:
+        - name: ssl
+          version: latest
+
+#. Enable billing for your App Engine project. Then enable socket support for
+   your app. This can be achieved by setting an environment variable in the
+   ``app.yaml`` file::
+
+        env_variables:
+          GAE_USE_SOCKETS_HTTPLIB : 'true'
+
+.. _App Engine standard environment:
+    https://cloud.google.com/appengine/docs/python
+.. _App Engine App Identity API:
+    https://cloud.google.com/appengine/docs/python/appidentity/
+.. _requests-toolbelt:
+    https://toolbelt.readthedocs.io/en/latest/
+.. _App Engine documentation:
+    https://cloud.google.com/appengine/docs/standard/python/issue-requests
+
+User credentials
+++++++++++++++++
+
+User credentials are typically obtained via `OAuth 2.0`_. This library does not
+provide any direct support for *obtaining* user credentials, however, you can
+use user credentials with this library. You can use libraries such as
+`oauthlib`_ to obtain the access token. After you have an access token, you
+can create a :class:`google.oauth2.credentials.Credentials` instance::
+
+    import google.oauth2.credentials
+
+    credentials = google.oauth2.credentials.Credentials(
+        'access_token')
+
+If you obtain a refresh token, you can also specify the refresh token and token
+URI to allow the credentials to be automatically refreshed::
+
+    credentials = google.oauth2.credentials.Credentials(
+        'access_token',
+        refresh_token='refresh_token',
+        token_uri='token_uri',
+        client_id='client_id',
+        client_secret='client_secret')
+
+
+There is a separate library, `google-auth-oauthlib`_, that has some helpers
+for integrating with `requests-oauthlib`_ to provide support for obtaining
+user credentials. You can use
+:func:`google_auth_oauthlib.helpers.credentials_from_session` to obtain
+:class:`google.oauth2.credentials.Credentials` from a 
+:class:`requests_oauthlib.OAuth2Session` as above::
+
+    from google_auth_oauthlib.helpers import credentials_from_session
+
+    google_auth_credentials = credentials_from_session(oauth2session)
+
+You can also use :class:`google_auth_oauthlib.flow.Flow` to perform the OAuth
+2.0 Authorization Grant Flow to obtain credentials using `requests-oauthlib`_.
+
+.. _OAuth 2.0:
+    https://developers.google.com/identity/protocols/OAuth2
+.. _oauthlib:
+    https://oauthlib.readthedocs.io/en/latest/
+.. _google-auth-oauthlib:
+    https://pypi.python.org/pypi/google-auth-oauthlib
+.. _requests-oauthlib:
+    https://requests-oauthlib.readthedocs.io/en/latest/
+
+External credentials (Workload identity federation)
++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+Using workload identity federation, your application can access Google Cloud
+resources from Amazon Web Services (AWS), Microsoft Azure or any identity
+provider that supports OpenID Connect (OIDC).
+
+Traditionally, applications running outside Google Cloud have used service
+account keys to access Google Cloud resources. Using identity federation,
+you can allow your workload to impersonate a service account.
+This lets you access Google Cloud resources directly, eliminating the
+maintenance and security burden associated with service account keys.
+
+Accessing resources from AWS
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In order to access Google Cloud resources from Amazon Web Services (AWS), the
+following requirements are needed:
+
+- A workload identity pool needs to be created.
+- AWS needs to be added as an identity provider in the workload identity pool
+  (The Google organization policy needs to allow federation from AWS).
+- Permission to impersonate a service account needs to be granted to the
+  external identity.
+- A credential configuration file needs to be generated. Unlike service account
+  credential files, the generated credential configuration file will only
+  contain non-sensitive metadata to instruct the library on how to retrieve
+  external subject tokens and exchange them for service account access tokens.
+
+Follow the detailed instructions on how to
+`Configure Workload Identity Federation from AWS`_.
+
+.. _Configure Workload Identity Federation from AWS:
+    https://cloud.google.com/iam/docs/access-resources-aws
+
+Accessing resources from Microsoft Azure
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In order to access Google Cloud resources from Microsoft Azure, the following
+requirements are needed:
+
+- A workload identity pool needs to be created.
+- Azure needs to be added as an identity provider in the workload identity pool
+  (The Google organization policy needs to allow federation from Azure).
+- The Azure tenant needs to be configured for identity federation.
+- Permission to impersonate a service account needs to be granted to the
+  external identity.
+- A credential configuration file needs to be generated. Unlike service account
+  credential files, the generated credential configuration file will only
+  contain non-sensitive metadata to instruct the library on how to retrieve
+  external subject tokens and exchange them for service account access tokens.
+
+Follow the detailed instructions on how to
+`Configure Workload Identity Federation from Microsoft Azure`_.
+
+.. _Configure Workload Identity Federation from Microsoft Azure:
+    https://cloud.google.com/iam/docs/access-resources-azure
+
+Accessing resources from an OIDC identity provider
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In order to access Google Cloud resources from an identity provider that
+supports `OpenID Connect (OIDC)`_, the following requirements are needed:
+
+- A workload identity pool needs to be created.
+- An OIDC identity provider needs to be added in the workload identity pool
+  (The Google organization policy needs to allow federation from the identity
+  provider).
+- Permission to impersonate a service account needs to be granted to the
+  external identity.
+- A credential configuration file needs to be generated. Unlike service account
+  credential files, the generated credential configuration file will only
+  contain non-sensitive metadata to instruct the library on how to retrieve
+  external subject tokens and exchange them for service account access tokens.
+
+For OIDC providers, the Auth library can retrieve OIDC tokens either from a
+local file location (file-sourced credentials) or from a local server
+(URL-sourced credentials).
+
+- For file-sourced credentials, a background process needs to be continuously
+  refreshing the file location with a new OIDC token prior to expiration.
+  For tokens with one hour lifetimes, the token needs to be updated in the file
+  every hour. The token can be stored directly as plain text or in JSON format.
+- For URL-sourced credentials, a local server needs to host a GET endpoint to
+  return the OIDC token. The response can be in plain text or JSON.
+  Additional required request headers can also be specified.
+
+Follow the detailed instructions on how to
+`Configure Workload Identity Federation from an OIDC identity provider`_.
+
+.. _OpenID Connect (OIDC):
+    https://openid.net/connect/
+.. _Configure Workload Identity Federation from an OIDC identity provider:
+    https://cloud.google.com/iam/docs/access-resources-oidc
+
+Using External Identities
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+External identities (AWS, Azure and OIDC identity providers) can be used with
+Application Default Credentials.
+In order to use external identities with Application Default Credentials, you
+need to generate the JSON credentials configuration file for your external
+identity.
+Once generated, store the path to this file in the
+``GOOGLE_APPLICATION_CREDENTIALS`` environment variable.
+
+.. code-block:: bash
+
+    $ export GOOGLE_APPLICATION_CREDENTIALS=/path/to/config.json
+
+The library can now automatically choose the right type of client and initialize
+credentials from the context provided in the configuration file::
+
+    import google.auth
+
+    credentials, project = google.auth.default()
+
+When using external identities with Application Default Credentials,
+the ``roles/browser`` role needs to be granted to the service account.
+The ``Cloud Resource Manager API`` should also be enabled on the project.
+This is needed since :func:`default` will try to auto-discover the project ID
+from the current environment using the impersonated credential.
+Otherwise, the project ID will resolve to ``None``. You can override the project
+detection by setting the ``GOOGLE_CLOUD_PROJECT`` environment variable.
+
+You can also explicitly initialize external account clients using the generated
+configuration file.
+
+For Azure and OIDC providers, use :meth:`identity_pool.Credentials.from_info
+<google.auth.identity_pool.Credentials.from_info>` or
+:meth:`identity_pool.Credentials.from_file
+<google.auth.identity_pool.Credentials.from_file>`::
+
+    import json
+
+    from google.auth import identity_pool
+
+    json_config_info = json.loads(function_to_get_json_config())
+    credentials = identity_pool.Credentials.from_info(json_config_info)
+    scoped_credentials = credentials.with_scopes(
+        ['https://www.googleapis.com/auth/cloud-platform'])
+
+For AWS providers, use :meth:`aws.Credentials.from_info
+<google.auth.aws.Credentials.from_info>` or
+:meth:`aws.Credentials.from_file
+<google.auth.aws.Credentials.from_file>`::
+
+    import json
+
+    from google.auth import aws
+
+    json_config_info = json.loads(function_to_get_json_config())
+    credentials = aws.Credentials.from_info(json_config_info)
+    scoped_credentials = credentials.with_scopes(
+        ['https://www.googleapis.com/auth/cloud-platform'])
+
+
+Impersonated credentials
+++++++++++++++++++++++++
+
+Impersonated Credentials allows one set of credentials issued to a user or service account
+to impersonate another.  The source credentials must be granted 
+the "Service Account Token Creator" IAM role. ::
+
+    from google.auth import impersonated_credentials
+
+    target_scopes = ['https://www.googleapis.com/auth/devstorage.read_only']
+    source_credentials = service_account.Credentials.from_service_account_file(
+        '/path/to/svc_account.json',
+        scopes=target_scopes)
+
+    target_credentials = impersonated_credentials.Credentials(
+        source_credentials=source_credentials,
+        target_principal='impersonated-account@_project_.iam.gserviceaccount.com',
+        target_scopes=target_scopes,
+        lifetime=500)
+    client = storage.Client(credentials=target_credentials)
+    buckets = client.list_buckets(project='your_project')
+    for bucket in buckets:
+        print(bucket.name)
+
+
+In the example above `source_credentials` does not have direct access to list buckets
+in the target project.  Using `ImpersonatedCredentials` will allow the source_credentials
+to assume the identity of a target_principal that does have access.
+
+
+Downscoped credentials
+++++++++++++++++++++++
+
+`Downscoping with Credential Access Boundaries`_ is used to restrict the
+Identity and Access Management (IAM) permissions that a short-lived credential
+can use.
+
+To downscope permissions of a source credential, a `Credential Access Boundary`
+that specifies which resources the new credential can access, as well as
+an upper bound on the permissions that are available on each resource, has to
+be defined. A downscoped credential can then be instantiated using the
+`source_credential` and the `Credential Access Boundary`.
+
+The common pattern of usage is to have a token broker with elevated access
+generate these downscoped credentials from higher access source credentials and
+pass the downscoped short-lived access tokens to a token consumer via some
+secure authenticated channel for limited access to Google Cloud Storage
+resources.
+
+.. _Downscoping with Credential Access Boundaries: https://cloud.google.com/iam/docs/downscoping-short-lived-credentials
+
+Token broker ::
+
+    import google.auth
+
+    from google.auth import downscoped
+    from google.auth.transport import requests
+
+    # Initialize the credential access boundary rules.
+    available_resource = '//storage.googleapis.com/projects/_/buckets/bucket-123'
+    available_permissions = ['inRole:roles/storage.objectViewer']
+    availability_expression = (
+        "resource.name.startsWith('projects/_/buckets/bucket-123/objects/customer-a')"
+    )
+
+    availability_condition = downscoped.AvailabilityCondition(
+        availability_expression)
+    rule = downscoped.AccessBoundaryRule(
+        available_resource=available_resource,
+        available_permissions=available_permissions,
+        availability_condition=availability_condition)
+    credential_access_boundary = downscoped.CredentialAccessBoundary(
+        rules=[rule])
+
+    # Retrieve the source credentials via ADC. 
+    source_credentials, _ = google.auth.default()
+
+    # Create the downscoped credentials.
+    downscoped_credentials = downscoped.Credentials(
+        source_credentials=source_credentials,
+        credential_access_boundary=credential_access_boundary)
+
+    # Refresh the tokens.
+    downscoped_credentials.refresh(requests.Request())
+
+    # These values will need to be passed to the Token Consumer.
+    access_token = downscoped_credentials.token
+    expiry = downscoped_credentials.expiry
+
+
+For example, a token broker can be set up on a server in a private network.
+Various workloads (token consumers) in the same network will send authenticated
+requests to that broker for downscoped tokens to access or modify specific google
+cloud storage buckets.
+
+The broker will instantiate downscoped credentials instances that can be used to
+generate short lived downscoped access tokens that can be passed to the token
+consumer. These downscoped access tokens can be injected by the consumer into
+`google.oauth2.Credentials` and used to initialize a storage client instance to
+access Google Cloud Storage resources with restricted access.
+
+Token Consumer ::
+
+    import google.oauth2
+
+    from google.auth.transport import requests
+    from google.cloud import storage
+
+    # Downscoped token retrieved from token broker.
+    # The `get_token_from_broker` callable requests a token and an expiry
+    # from the token broker.
+    downscoped_token, expiry = get_token_from_broker(
+        requests.Request(),
+        scopes=['https://www.googleapis.com/auth/cloud-platform'])
+
+    # Create the OAuth credentials from the downscoped token and pass a
+    # refresh handler to handle token expiration. Passing the original
+    # downscoped token or the expiry here is optional, as the refresh_handler
+    # will generate the downscoped token on demand.
+    credentials = google.oauth2.credentials.Credentials(
+        downscoped_token,
+        expiry=expiry,
+        scopes=['https://www.googleapis.com/auth/cloud-platform'],
+        refresh_handler=get_token_from_broker)
+
+    # Initialize a storage client with the oauth2 credentials.
+    storage_client = storage.Client(
+        project='my_project_id', credentials=credentials)
+    # Call GCS APIs.
+    # The token broker has readonly access to objects starting with "customer-a"
+    # in bucket "bucket-123".
+    bucket = storage_client.bucket('bucket-123')
+    blob = bucket.blob('customer-a-data.txt')
+    print(blob.download_as_bytes().decode("utf-8"))
+
+
+Another reason to use downscoped credentials is to ensure tokens in flight
+always have the least privileges, e.g. Principle of Least Privilege. ::
+
+    # Create the downscoped credentials.
+    downscoped_credentials = downscoped.Credentials(
+        # source_credentials have elevated access but only a subset of
+        # these permissions are needed here.
+        source_credentials=source_credentials,
+        credential_access_boundary=credential_access_boundary)
+
+    # Pass the token directly.
+    storage_client = storage.Client(
+        project='my_project_id', credentials=downscoped_credentials)
+    # If the source credentials have elevated levels of access, the
+    # token in flight here will have limited readonly access to objects
+    # starting with "customer-a" in bucket "bucket-123".
+    bucket = storage_client.bucket('bucket-123')
+    blob = bucket.blob('customer-a-data.txt')
+    print(blob.download_as_string())
+
+
+Note: Only Cloud Storage supports Credential Access Boundaries. Other Google
+Cloud services do not support this feature.
+
+
+Identity Tokens
++++++++++++++++
+
+`Google OpenID Connect`_ tokens are available through :mod:`Service Account <google.oauth2.service_account>`,
+:mod:`Impersonated <google.auth.impersonated_credentials>`,
+and :mod:`Compute Engine <google.auth.compute_engine>`.  These tokens can be used to
+authenticate against `Cloud Functions`_, `Cloud Run`_, a user service behind
+`Identity Aware Proxy`_ or any other service capable of verifying a `Google ID Token`_.
+
+ServiceAccount ::
+
+    from google.oauth2 import service_account
+
+    target_audience = 'https://example.com'
+
+    creds = service_account.IDTokenCredentials.from_service_account_file(
+            '/path/to/svc.json',
+            target_audience=target_audience)
+
+
+Compute ::
+
+    from google.auth import compute_engine
+    import google.auth.transport.requests
+
+    target_audience = 'https://example.com'
+
+    request = google.auth.transport.requests.Request()
+    creds = compute_engine.IDTokenCredentials(request,
+                            target_audience=target_audience)
+
+Impersonated ::
+
+    from google.auth import impersonated_credentials
+
+    # get target_credentials from a source_credential
+
+    target_audience = 'https://example.com'
+
+    creds = impersonated_credentials.IDTokenCredentials(
+                                      target_credentials,
+                                      target_audience=target_audience)
+
+If your application runs on `App Engine`_, `Cloud Run`_, `Compute Engine`_, or
+has application default credentials set via `GOOGLE_APPLICATION_CREDENTIALS`
+environment variable, you can also use `google.oauth2.id_token.fetch_id_token`
+to obtain an ID token from your current running environment. The following is an
+example ::
+
+    import google.oauth2.id_token
+    import google.auth.transport.requests
+
+    request = google.auth.transport.requests.Request()
+    target_audience = "https://pubsub.googleapis.com"
+
+    id_token = google.oauth2.id_token.fetch_id_token(request, target_audience)
+
+IDToken verification can be done for various type of IDTokens using the
+:class:`google.oauth2.id_token` module. It supports ID token signed with RS256
+and ES256 algorithms. However, ES256 algorithm won't be available unless
+`cryptography` dependency of version at least 1.4.0 is installed. You can check
+the dependency with `pip freeze` or try `from google.auth.crypt import es256`.
+The following is an example of verifying ID tokens ::
+
+    from google.auth2 import id_token
+
+    request = google.auth.transport.requests.Request()
+
+    try:
+        decoded_token = id_token.verify_token(token_to_verify,request)
+    except ValueError:
+        # Verification failed.
+
+A sample end-to-end flow using an ID Token against a Cloud Run endpoint maybe ::
+
+    from google.oauth2 import id_token
+    from google.oauth2 import service_account
+    import google.auth
+    import google.auth.transport.requests
+    from google.auth.transport.requests import AuthorizedSession
+
+    target_audience = 'https://your-cloud-run-app.a.run.app'
+    url = 'https://your-cloud-run-app.a.run.app'
+
+    creds = service_account.IDTokenCredentials.from_service_account_file(
+            '/path/to/svc.json', target_audience=target_audience)
+
+    authed_session = AuthorizedSession(creds)
+
+    # make authenticated request and print the response, status_code
+    resp = authed_session.get(url)
+    print(resp.status_code)
+    print(resp.text)
+
+    # to verify an ID Token
+    request = google.auth.transport.requests.Request()
+    token = creds.token
+    print(token)
+    print(id_token.verify_token(token,request))
+
+.. _App Engine: https://cloud.google.com/appengine/
+.. _Cloud Functions: https://cloud.google.com/functions/
+.. _Cloud Run: https://cloud.google.com/run/
+.. _Identity Aware Proxy: https://cloud.google.com/iap/
+.. _Google OpenID Connect: https://developers.google.com/identity/protocols/OpenIDConnect
+.. _Google ID Token: https://developers.google.com/identity/protocols/OpenIDConnect#validatinganidtoken
+
+Making authenticated requests
+-----------------------------
+
+Once you have credentials you can attach them to a *transport*. You can then
+use this transport to make authenticated requests to APIs. google-auth supports
+several different transports. Typically, it's up to your application or an
+opinionated client library to decide which transport to use.
+
+Requests
+++++++++
+
+The recommended HTTP transport is :mod:`google.auth.transport.requests` which
+uses the `Requests`_ library. To make authenticated requests using Requests
+you use a custom `Session`_ object::
+
+    from google.auth.transport.requests import AuthorizedSession
+
+    authed_session = AuthorizedSession(credentials)
+
+    response = authed_session.get(
+        'https://www.googleapis.com/storage/v1/b')
+
+.. _Requests: http://docs.python-requests.org/en/master/
+.. _Session: http://docs.python-requests.org/en/master/user/advanced/#session-objects
+
+urllib3
++++++++
+
+:mod:`urllib3` is the underlying HTTP library used by Requests and can also be
+used with google-auth. urllib3's interface isn't as high-level as Requests but
+it can be useful in situations where you need more control over how HTTP
+requests are made. To make authenticated requests using urllib3 create an
+instance of :class:`google.auth.transport.urllib3.AuthorizedHttp`::
+
+    from google.auth.transport.urllib3 import AuthorizedHttp
+
+    authed_http = AuthorizedHttp(credentials)
+
+    response = authed_http.request(
+        'GET', 'https://www.googleapis.com/storage/v1/b')
+
+You can also construct your own :class:`urllib3.PoolManager` instance and pass
+it to :class:`~google.auth.transport.urllib3.AuthorizedHttp`::
+
+    import urllib3
+
+    http = urllib3.PoolManager()
+    authed_http = AuthorizedHttp(credentials, http)
+
+gRPC
+++++
+
+`gRPC`_ is an RPC framework that uses `Protocol Buffers`_ over `HTTP 2.0`_.
+google-auth can provide `Call Credentials`_ for gRPC. The easiest way to do
+this is to use google-auth to create the gRPC channel::
+
+    import google.auth.transport.grpc
+    import google.auth.transport.requests
+
+    http_request = google.auth.transport.requests.Request()
+
+    channel = google.auth.transport.grpc.secure_authorized_channel(
+        credentials, http_request, 'pubsub.googleapis.com:443')
+
+.. note:: Even though gRPC is its own transport, you still need to use one of
+    the other HTTP transports with gRPC. The reason is that most credential
+    types need to make HTTP requests in order to refresh their access token.
+    The sample above uses the Requests transport, but any HTTP transport can
+    be used. Additionally, if you know that your credentials do not need to
+    make HTTP requests in order to refresh (as is the case with
+    :class:`jwt.Credentials`) then you can specify ``None``.
+
+Alternatively, you can create the channel yourself and use
+:class:`google.auth.transport.grpc.AuthMetadataPlugin`::
+
+    import grpc
+
+    metadata_plugin = AuthMetadataPlugin(credentials, http_request)
+
+    # Create a set of grpc.CallCredentials using the metadata plugin.
+    google_auth_credentials = grpc.metadata_call_credentials(
+        metadata_plugin)
+
+    # Create SSL channel credentials.
+    ssl_credentials = grpc.ssl_channel_credentials()
+
+    # Combine the ssl credentials and the authorization credentials.
+    composite_credentials = grpc.composite_channel_credentials(
+        ssl_credentials, google_auth_credentials)
+
+    channel = grpc.secure_channel(
+        'pubsub.googleapis.com:443', composite_credentials)
+
+You can use this channel to make a gRPC stub that makes authenticated requests
+to a gRPC service::
+
+    from google.pubsub.v1 import pubsub_pb2
+
+    pubsub = pubsub_pb2.PublisherStub(channel)
+
+    response = pubsub.ListTopics(
+        pubsub_pb2.ListTopicsRequest(project='your-project'))
+
+
+.. _gRPC: http://www.grpc.io/
+.. _Protocol Buffers:
+    https://developers.google.com/protocol-buffers/docs/overview
+.. _HTTP 2.0:
+    http://www.grpc.io/docs/guides/wire.html
+.. _Call Credentials:
+    http://www.grpc.io/docs/guides/auth.html
diff --git a/google/__init__.py b/google/__init__.py
new file mode 100644
index 0000000..0d0a4c3
--- /dev/null
+++ b/google/__init__.py
@@ -0,0 +1,24 @@
+# Copyright 2016 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.
+
+"""Google namespace package."""
+
+try:
+    import pkg_resources
+
+    pkg_resources.declare_namespace(__name__)
+except ImportError:
+    import pkgutil
+
+    __path__ = pkgutil.extend_path(__path__, __name__)
diff --git a/google/auth/__init__.py b/google/auth/__init__.py
new file mode 100644
index 0000000..861abe7
--- /dev/null
+++ b/google/auth/__init__.py
@@ -0,0 +1,29 @@
+# Copyright 2016 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.
+
+"""Google Auth Library for Python."""
+
+import logging
+
+from google.auth import version as google_auth_version
+from google.auth._default import default, load_credentials_from_file
+
+
+__version__ = google_auth_version.__version__
+
+
+__all__ = ["default", "load_credentials_from_file"]
+
+# Set default logging handler to avoid "No handler found" warnings.
+logging.getLogger(__name__).addHandler(logging.NullHandler())
diff --git a/google/auth/_cloud_sdk.py b/google/auth/_cloud_sdk.py
new file mode 100644
index 0000000..40e6aec
--- /dev/null
+++ b/google/auth/_cloud_sdk.py
@@ -0,0 +1,159 @@
+# Copyright 2015 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Helpers for reading the Google Cloud SDK's configuration."""
+
+import json
+import os
+import subprocess
+
+import six
+
+from google.auth import environment_vars
+from google.auth import exceptions
+
+
+# The ~/.config subdirectory containing gcloud credentials.
+_CONFIG_DIRECTORY = "gcloud"
+# Windows systems store config at %APPDATA%\gcloud
+_WINDOWS_CONFIG_ROOT_ENV_VAR = "APPDATA"
+# The name of the file in the Cloud SDK config that contains default
+# credentials.
+_CREDENTIALS_FILENAME = "application_default_credentials.json"
+# The name of the Cloud SDK shell script
+_CLOUD_SDK_POSIX_COMMAND = "gcloud"
+_CLOUD_SDK_WINDOWS_COMMAND = "gcloud.cmd"
+# The command to get the Cloud SDK configuration
+_CLOUD_SDK_CONFIG_COMMAND = ("config", "config-helper", "--format", "json")
+# The command to get google user access token
+_CLOUD_SDK_USER_ACCESS_TOKEN_COMMAND = ("auth", "print-access-token")
+# Cloud SDK's application-default client ID
+CLOUD_SDK_CLIENT_ID = (
+    "764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.apps.googleusercontent.com"
+)
+
+
+def get_config_path():
+    """Returns the absolute path the the Cloud SDK's configuration directory.
+
+    Returns:
+        str: The Cloud SDK config path.
+    """
+    # If the path is explicitly set, return that.
+    try:
+        return os.environ[environment_vars.CLOUD_SDK_CONFIG_DIR]
+    except KeyError:
+        pass
+
+    # Non-windows systems store this at ~/.config/gcloud
+    if os.name != "nt":
+        return os.path.join(os.path.expanduser("~"), ".config", _CONFIG_DIRECTORY)
+    # Windows systems store config at %APPDATA%\gcloud
+    else:
+        try:
+            return os.path.join(
+                os.environ[_WINDOWS_CONFIG_ROOT_ENV_VAR], _CONFIG_DIRECTORY
+            )
+        except KeyError:
+            # This should never happen unless someone is really
+            # messing with things, but we'll cover the case anyway.
+            drive = os.environ.get("SystemDrive", "C:")
+            return os.path.join(drive, "\\", _CONFIG_DIRECTORY)
+
+
+def get_application_default_credentials_path():
+    """Gets the path to the application default credentials file.
+
+    The path may or may not exist.
+
+    Returns:
+        str: The full path to application default credentials.
+    """
+    config_path = get_config_path()
+    return os.path.join(config_path, _CREDENTIALS_FILENAME)
+
+
+def _run_subprocess_ignore_stderr(command):
+    """ Return subprocess.check_output with the given command and ignores stderr."""
+    with open(os.devnull, "w") as devnull:
+        output = subprocess.check_output(command, stderr=devnull)
+    return output
+
+
+def get_project_id():
+    """Gets the project ID from the Cloud SDK.
+
+    Returns:
+        Optional[str]: The project ID.
+    """
+    if os.name == "nt":
+        command = _CLOUD_SDK_WINDOWS_COMMAND
+    else:
+        command = _CLOUD_SDK_POSIX_COMMAND
+
+    try:
+        # Ignore the stderr coming from gcloud, so it won't be mixed into the output.
+        # https://github.com/googleapis/google-auth-library-python/issues/673
+        output = _run_subprocess_ignore_stderr((command,) + _CLOUD_SDK_CONFIG_COMMAND)
+    except (subprocess.CalledProcessError, OSError, IOError):
+        return None
+
+    try:
+        configuration = json.loads(output.decode("utf-8"))
+    except ValueError:
+        return None
+
+    try:
+        return configuration["configuration"]["properties"]["core"]["project"]
+    except KeyError:
+        return None
+
+
+def get_auth_access_token(account=None):
+    """Load user access token with the ``gcloud auth print-access-token`` command.
+
+    Args:
+        account (Optional[str]): Account to get the access token for. If not
+            specified, the current active account will be used.
+
+    Returns:
+        str: The user access token.
+
+    Raises:
+        google.auth.exceptions.UserAccessTokenError: if failed to get access
+            token from gcloud.
+    """
+    if os.name == "nt":
+        command = _CLOUD_SDK_WINDOWS_COMMAND
+    else:
+        command = _CLOUD_SDK_POSIX_COMMAND
+
+    try:
+        if account:
+            command = (
+                (command,)
+                + _CLOUD_SDK_USER_ACCESS_TOKEN_COMMAND
+                + ("--account=" + account,)
+            )
+        else:
+            command = (command,) + _CLOUD_SDK_USER_ACCESS_TOKEN_COMMAND
+
+        access_token = subprocess.check_output(command, stderr=subprocess.STDOUT)
+        # remove the trailing "\n"
+        return access_token.decode("utf-8").strip()
+    except (subprocess.CalledProcessError, OSError, IOError) as caught_exc:
+        new_exc = exceptions.UserAccessTokenError(
+            "Failed to obtain access token", caught_exc
+        )
+        six.raise_from(new_exc, caught_exc)
diff --git a/google/auth/_credentials_async.py b/google/auth/_credentials_async.py
new file mode 100644
index 0000000..d4d4e2c
--- /dev/null
+++ b/google/auth/_credentials_async.py
@@ -0,0 +1,176 @@
+# 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.
+
+
+"""Interfaces for credentials."""
+
+import abc
+import inspect
+
+import six
+
+from google.auth import credentials
+
+
[email protected]_metaclass(abc.ABCMeta)
+class Credentials(credentials.Credentials):
+    """Async inherited credentials class from google.auth.credentials.
+    The added functionality is the before_request call which requires
+    async/await syntax.
+    All credentials have a :attr:`token` that is used for authentication and
+    may also optionally set an :attr:`expiry` to indicate when the token will
+    no longer be valid.
+
+    Most credentials will be :attr:`invalid` until :meth:`refresh` is called.
+    Credentials can do this automatically before the first HTTP request in
+    :meth:`before_request`.
+
+    Although the token and expiration will change as the credentials are
+    :meth:`refreshed <refresh>` and used, credentials should be considered
+    immutable. Various credentials will accept configuration such as private
+    keys, scopes, and other options. These options are not changeable after
+    construction. Some classes will provide mechanisms to copy the credentials
+    with modifications such as :meth:`ScopedCredentials.with_scopes`.
+    """
+
+    async def before_request(self, request, method, url, headers):
+        """Performs credential-specific before request logic.
+
+        Refreshes the credentials if necessary, then calls :meth:`apply` to
+        apply the token to the authentication header.
+
+        Args:
+            request (google.auth.transport.Request): The object used to make
+                HTTP requests.
+            method (str): The request's HTTP method or the RPC method being
+                invoked.
+            url (str): The request's URI or the RPC service's URI.
+            headers (Mapping): The request's headers.
+        """
+        # pylint: disable=unused-argument
+        # (Subclasses may use these arguments to ascertain information about
+        # the http request.)
+
+        if not self.valid:
+            if inspect.iscoroutinefunction(self.refresh):
+                await self.refresh(request)
+            else:
+                self.refresh(request)
+        self.apply(headers)
+
+
+class CredentialsWithQuotaProject(credentials.CredentialsWithQuotaProject):
+    """Abstract base for credentials supporting ``with_quota_project`` factory"""
+
+
+class AnonymousCredentials(credentials.AnonymousCredentials, Credentials):
+    """Credentials that do not provide any authentication information.
+
+    These are useful in the case of services that support anonymous access or
+    local service emulators that do not use credentials. This class inherits
+    from the sync anonymous credentials file, but is kept if async credentials
+    is initialized and we would like anonymous credentials.
+    """
+
+
[email protected]_metaclass(abc.ABCMeta)
+class ReadOnlyScoped(credentials.ReadOnlyScoped):
+    """Interface for credentials whose scopes can be queried.
+
+    OAuth 2.0-based credentials allow limiting access using scopes as described
+    in `RFC6749 Section 3.3`_.
+    If a credential class implements this interface then the credentials either
+    use scopes in their implementation.
+
+    Some credentials require scopes in order to obtain a token. You can check
+    if scoping is necessary with :attr:`requires_scopes`::
+
+        if credentials.requires_scopes:
+            # Scoping is required.
+            credentials = _credentials_async.with_scopes(scopes=['one', 'two'])
+
+    Credentials that require scopes must either be constructed with scopes::
+
+        credentials = SomeScopedCredentials(scopes=['one', 'two'])
+
+    Or must copy an existing instance using :meth:`with_scopes`::
+
+        scoped_credentials = _credentials_async.with_scopes(scopes=['one', 'two'])
+
+    Some credentials have scopes but do not allow or require scopes to be set,
+    these credentials can be used as-is.
+
+    .. _RFC6749 Section 3.3: https://tools.ietf.org/html/rfc6749#section-3.3
+    """
+
+
+class Scoped(credentials.Scoped):
+    """Interface for credentials whose scopes can be replaced while copying.
+
+    OAuth 2.0-based credentials allow limiting access using scopes as described
+    in `RFC6749 Section 3.3`_.
+    If a credential class implements this interface then the credentials either
+    use scopes in their implementation.
+
+    Some credentials require scopes in order to obtain a token. You can check
+    if scoping is necessary with :attr:`requires_scopes`::
+
+        if credentials.requires_scopes:
+            # Scoping is required.
+            credentials = _credentials_async.create_scoped(['one', 'two'])
+
+    Credentials that require scopes must either be constructed with scopes::
+
+        credentials = SomeScopedCredentials(scopes=['one', 'two'])
+
+    Or must copy an existing instance using :meth:`with_scopes`::
+
+        scoped_credentials = credentials.with_scopes(scopes=['one', 'two'])
+
+    Some credentials have scopes but do not allow or require scopes to be set,
+    these credentials can be used as-is.
+
+    .. _RFC6749 Section 3.3: https://tools.ietf.org/html/rfc6749#section-3.3
+    """
+
+
+def with_scopes_if_required(credentials, scopes):
+    """Creates a copy of the credentials with scopes if scoping is required.
+
+    This helper function is useful when you do not know (or care to know) the
+    specific type of credentials you are using (such as when you use
+    :func:`google.auth.default`). This function will call
+    :meth:`Scoped.with_scopes` if the credentials are scoped credentials and if
+    the credentials require scoping. Otherwise, it will return the credentials
+    as-is.
+
+    Args:
+        credentials (google.auth.credentials.Credentials): The credentials to
+            scope if necessary.
+        scopes (Sequence[str]): The list of scopes to use.
+
+    Returns:
+        google.auth._credentials_async.Credentials: Either a new set of scoped
+            credentials, or the passed in credentials instance if no scoping
+            was required.
+    """
+    if isinstance(credentials, Scoped) and credentials.requires_scopes:
+        return credentials.with_scopes(scopes)
+    else:
+        return credentials
+
+
[email protected]_metaclass(abc.ABCMeta)
+class Signing(credentials.Signing):
+    """Interface for credentials that can cryptographically sign messages."""
diff --git a/google/auth/_default.py b/google/auth/_default.py
new file mode 100644
index 0000000..4ae7c8c
--- /dev/null
+++ b/google/auth/_default.py
@@ -0,0 +1,493 @@
+# Copyright 2015 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Application default credentials.
+
+Implements application default credentials and project ID detection.
+"""
+
+import io
+import json
+import logging
+import os
+import warnings
+
+import six
+
+from google.auth import environment_vars
+from google.auth import exceptions
+import google.auth.transport._http_client
+
+_LOGGER = logging.getLogger(__name__)
+
+# Valid types accepted for file-based credentials.
+_AUTHORIZED_USER_TYPE = "authorized_user"
+_SERVICE_ACCOUNT_TYPE = "service_account"
+_EXTERNAL_ACCOUNT_TYPE = "external_account"
+_VALID_TYPES = (_AUTHORIZED_USER_TYPE, _SERVICE_ACCOUNT_TYPE, _EXTERNAL_ACCOUNT_TYPE)
+
+# Help message when no credentials can be found.
+_HELP_MESSAGE = """\
+Could not automatically determine credentials. Please set {env} or \
+explicitly create credentials and re-run the application. For more \
+information, please see \
+https://cloud.google.com/docs/authentication/getting-started
+""".format(
+    env=environment_vars.CREDENTIALS
+).strip()
+
+# Warning when using Cloud SDK user credentials
+_CLOUD_SDK_CREDENTIALS_WARNING = """\
+Your application has authenticated using end user credentials from Google \
+Cloud SDK without a quota project. You might receive a "quota exceeded" \
+or "API not enabled" error. We recommend you rerun \
+`gcloud auth application-default login` and make sure a quota project is \
+added. Or you can use service accounts instead. For more information \
+about service accounts, see https://cloud.google.com/docs/authentication/"""
+
+# The subject token type used for AWS external_account credentials.
+_AWS_SUBJECT_TOKEN_TYPE = "urn:ietf:params:aws:token-type:aws4_request"
+
+
+def _warn_about_problematic_credentials(credentials):
+    """Determines if the credentials are problematic.
+
+    Credentials from the Cloud SDK that are associated with Cloud SDK's project
+    are problematic because they may not have APIs enabled and have limited
+    quota. If this is the case, warn about it.
+    """
+    from google.auth import _cloud_sdk
+
+    if credentials.client_id == _cloud_sdk.CLOUD_SDK_CLIENT_ID:
+        warnings.warn(_CLOUD_SDK_CREDENTIALS_WARNING)
+
+
+def load_credentials_from_file(
+    filename, scopes=None, default_scopes=None, quota_project_id=None, request=None
+):
+    """Loads Google credentials from a file.
+
+    The credentials file must be a service account key, stored authorized
+    user credentials or external account credentials.
+
+    Args:
+        filename (str): The full path to the credentials file.
+        scopes (Optional[Sequence[str]]): The list of scopes for the credentials. If
+            specified, the credentials will automatically be scoped if
+            necessary
+        default_scopes (Optional[Sequence[str]]): Default scopes passed by a
+            Google client library. Use 'scopes' for user-defined scopes.
+        quota_project_id (Optional[str]):  The project ID used for
+            quota and billing.
+        request (Optional[google.auth.transport.Request]): An object used to make
+            HTTP requests. This is used to determine the associated project ID
+            for a workload identity pool resource (external account credentials).
+            If not specified, then it will use a
+            google.auth.transport.requests.Request client to make requests.
+
+    Returns:
+        Tuple[google.auth.credentials.Credentials, Optional[str]]: Loaded
+            credentials and the project ID. Authorized user credentials do not
+            have the project ID information. External account credentials project
+            IDs may not always be determined.
+
+    Raises:
+        google.auth.exceptions.DefaultCredentialsError: if the file is in the
+            wrong format or is missing.
+    """
+    if not os.path.exists(filename):
+        raise exceptions.DefaultCredentialsError(
+            "File {} was not found.".format(filename)
+        )
+
+    with io.open(filename, "r") as file_obj:
+        try:
+            info = json.load(file_obj)
+        except ValueError as caught_exc:
+            new_exc = exceptions.DefaultCredentialsError(
+                "File {} is not a valid json file.".format(filename), caught_exc
+            )
+            six.raise_from(new_exc, caught_exc)
+
+    # The type key should indicate that the file is either a service account
+    # credentials file or an authorized user credentials file.
+    credential_type = info.get("type")
+
+    if credential_type == _AUTHORIZED_USER_TYPE:
+        from google.oauth2 import credentials
+
+        try:
+            credentials = credentials.Credentials.from_authorized_user_info(
+                info, scopes=scopes
+            )
+        except ValueError as caught_exc:
+            msg = "Failed to load authorized user credentials from {}".format(filename)
+            new_exc = exceptions.DefaultCredentialsError(msg, caught_exc)
+            six.raise_from(new_exc, caught_exc)
+        if quota_project_id:
+            credentials = credentials.with_quota_project(quota_project_id)
+        if not credentials.quota_project_id:
+            _warn_about_problematic_credentials(credentials)
+        return credentials, None
+
+    elif credential_type == _SERVICE_ACCOUNT_TYPE:
+        from google.oauth2 import service_account
+
+        try:
+            credentials = service_account.Credentials.from_service_account_info(
+                info, scopes=scopes, default_scopes=default_scopes
+            )
+        except ValueError as caught_exc:
+            msg = "Failed to load service account credentials from {}".format(filename)
+            new_exc = exceptions.DefaultCredentialsError(msg, caught_exc)
+            six.raise_from(new_exc, caught_exc)
+        if quota_project_id:
+            credentials = credentials.with_quota_project(quota_project_id)
+        return credentials, info.get("project_id")
+
+    elif credential_type == _EXTERNAL_ACCOUNT_TYPE:
+        credentials, project_id = _get_external_account_credentials(
+            info,
+            filename,
+            scopes=scopes,
+            default_scopes=default_scopes,
+            request=request,
+        )
+        if quota_project_id:
+            credentials = credentials.with_quota_project(quota_project_id)
+        return credentials, project_id
+
+    else:
+        raise exceptions.DefaultCredentialsError(
+            "The file {file} does not have a valid type. "
+            "Type is {type}, expected one of {valid_types}.".format(
+                file=filename, type=credential_type, valid_types=_VALID_TYPES
+            )
+        )
+
+
+def _get_gcloud_sdk_credentials(quota_project_id=None):
+    """Gets the credentials and project ID from the Cloud SDK."""
+    from google.auth import _cloud_sdk
+
+    _LOGGER.debug("Checking Cloud SDK credentials as part of auth process...")
+
+    # Check if application default credentials exist.
+    credentials_filename = _cloud_sdk.get_application_default_credentials_path()
+
+    if not os.path.isfile(credentials_filename):
+        _LOGGER.debug("Cloud SDK credentials not found on disk; not using them")
+        return None, None
+
+    credentials, project_id = load_credentials_from_file(
+        credentials_filename, quota_project_id=quota_project_id
+    )
+
+    if not project_id:
+        project_id = _cloud_sdk.get_project_id()
+
+    return credentials, project_id
+
+
+def _get_explicit_environ_credentials(quota_project_id=None):
+    """Gets credentials from the GOOGLE_APPLICATION_CREDENTIALS environment
+    variable."""
+    from google.auth import _cloud_sdk
+
+    cloud_sdk_adc_path = _cloud_sdk.get_application_default_credentials_path()
+    explicit_file = os.environ.get(environment_vars.CREDENTIALS)
+
+    _LOGGER.debug(
+        "Checking %s for explicit credentials as part of auth process...", explicit_file
+    )
+
+    if explicit_file is not None and explicit_file == cloud_sdk_adc_path:
+        # Cloud sdk flow calls gcloud to fetch project id, so if the explicit
+        # file path is cloud sdk credentials path, then we should fall back
+        # to cloud sdk flow, otherwise project id cannot be obtained.
+        _LOGGER.debug(
+            "Explicit credentials path %s is the same as Cloud SDK credentials path, fall back to Cloud SDK credentials flow...",
+            explicit_file,
+        )
+        return _get_gcloud_sdk_credentials(quota_project_id=quota_project_id)
+
+    if explicit_file is not None:
+        credentials, project_id = load_credentials_from_file(
+            os.environ[environment_vars.CREDENTIALS], quota_project_id=quota_project_id
+        )
+
+        return credentials, project_id
+
+    else:
+        return None, None
+
+
+def _get_gae_credentials():
+    """Gets Google App Engine App Identity credentials and project ID."""
+    # If not GAE gen1, prefer the metadata service even if the GAE APIs are
+    # available as per https://google.aip.dev/auth/4115.
+    if os.environ.get(environment_vars.LEGACY_APPENGINE_RUNTIME) != "python27":
+        return None, None
+
+    # While this library is normally bundled with app_engine, there are
+    # some cases where it's not available, so we tolerate ImportError.
+    try:
+        _LOGGER.debug("Checking for App Engine runtime as part of auth process...")
+        import google.auth.app_engine as app_engine
+    except ImportError:
+        _LOGGER.warning("Import of App Engine auth library failed.")
+        return None, None
+
+    try:
+        credentials = app_engine.Credentials()
+        project_id = app_engine.get_project_id()
+        return credentials, project_id
+    except EnvironmentError:
+        _LOGGER.debug(
+            "No App Engine library was found so cannot authentication via App Engine Identity Credentials."
+        )
+        return None, None
+
+
+def _get_gce_credentials(request=None):
+    """Gets credentials and project ID from the GCE Metadata Service."""
+    # Ping requires a transport, but we want application default credentials
+    # to require no arguments. So, we'll use the _http_client transport which
+    # uses http.client. This is only acceptable because the metadata server
+    # doesn't do SSL and never requires proxies.
+
+    # While this library is normally bundled with compute_engine, there are
+    # some cases where it's not available, so we tolerate ImportError.
+    try:
+        from google.auth import compute_engine
+        from google.auth.compute_engine import _metadata
+    except ImportError:
+        _LOGGER.warning("Import of Compute Engine auth library failed.")
+        return None, None
+
+    if request is None:
+        request = google.auth.transport._http_client.Request()
+
+    if _metadata.ping(request=request):
+        # Get the project ID.
+        try:
+            project_id = _metadata.get_project_id(request=request)
+        except exceptions.TransportError:
+            project_id = None
+
+        return compute_engine.Credentials(), project_id
+    else:
+        _LOGGER.warning(
+            "Authentication failed using Compute Engine authentication due to unavailable metadata server."
+        )
+        return None, None
+
+
+def _get_external_account_credentials(
+    info, filename, scopes=None, default_scopes=None, request=None
+):
+    """Loads external account Credentials from the parsed external account info.
+
+    The credentials information must correspond to a supported external account
+    credentials.
+
+    Args:
+        info (Mapping[str, str]): The external account info in Google format.
+        filename (str): The full path to the credentials file.
+        scopes (Optional[Sequence[str]]): The list of scopes for the credentials. If
+            specified, the credentials will automatically be scoped if
+            necessary.
+        default_scopes (Optional[Sequence[str]]): Default scopes passed by a
+            Google client library. Use 'scopes' for user-defined scopes.
+        request (Optional[google.auth.transport.Request]): An object used to make
+            HTTP requests. This is used to determine the associated project ID
+            for a workload identity pool resource (external account credentials).
+            If not specified, then it will use a
+            google.auth.transport.requests.Request client to make requests.
+
+    Returns:
+        Tuple[google.auth.credentials.Credentials, Optional[str]]: Loaded
+            credentials and the project ID. External account credentials project
+            IDs may not always be determined.
+
+    Raises:
+        google.auth.exceptions.DefaultCredentialsError: if the info dictionary
+            is in the wrong format or is missing required information.
+    """
+    # There are currently 2 types of external_account credentials.
+    if info.get("subject_token_type") == _AWS_SUBJECT_TOKEN_TYPE:
+        # Check if configuration corresponds to an AWS credentials.
+        from google.auth import aws
+
+        credentials = aws.Credentials.from_info(
+            info, scopes=scopes, default_scopes=default_scopes
+        )
+    else:
+        try:
+            # Check if configuration corresponds to an Identity Pool credentials.
+            from google.auth import identity_pool
+
+            credentials = identity_pool.Credentials.from_info(
+                info, scopes=scopes, default_scopes=default_scopes
+            )
+        except ValueError:
+            # If the configuration is invalid or does not correspond to any
+            # supported external_account credentials, raise an error.
+            raise exceptions.DefaultCredentialsError(
+                "Failed to load external account credentials from {}".format(filename)
+            )
+    if request is None:
+        request = google.auth.transport.requests.Request()
+
+    return credentials, credentials.get_project_id(request=request)
+
+
+def default(scopes=None, request=None, quota_project_id=None, default_scopes=None):
+    """Gets the default credentials for the current environment.
+
+    `Application Default Credentials`_ provides an easy way to obtain
+    credentials to call Google APIs for server-to-server or local applications.
+    This function acquires credentials from the environment in the following
+    order:
+
+    1. If the environment variable ``GOOGLE_APPLICATION_CREDENTIALS`` is set
+       to the path of a valid service account JSON private key file, then it is
+       loaded and returned. The project ID returned is the project ID defined
+       in the service account file if available (some older files do not
+       contain project ID information).
+
+       If the environment variable is set to the path of a valid external
+       account JSON configuration file (workload identity federation), then the
+       configuration file is used to determine and retrieve the external
+       credentials from the current environment (AWS, Azure, etc).
+       These will then be exchanged for Google access tokens via the Google STS
+       endpoint.
+       The project ID returned in this case is the one corresponding to the
+       underlying workload identity pool resource if determinable.
+    2. If the `Google Cloud SDK`_ is installed and has application default
+       credentials set they are loaded and returned.
+
+       To enable application default credentials with the Cloud SDK run::
+
+            gcloud auth application-default login
+
+       If the Cloud SDK has an active project, the project ID is returned. The
+       active project can be set using::
+
+            gcloud config set project
+
+    3. If the application is running in the `App Engine standard environment`_
+       (first generation) then the credentials and project ID from the
+       `App Identity Service`_ are used.
+    4. If the application is running in `Compute Engine`_ or `Cloud Run`_ or
+       the `App Engine flexible environment`_ or the `App Engine standard
+       environment`_ (second generation) then the credentials and project ID
+       are obtained from the `Metadata Service`_.
+    5. If no credentials are found,
+       :class:`~google.auth.exceptions.DefaultCredentialsError` will be raised.
+
+    .. _Application Default Credentials: https://developers.google.com\
+            /identity/protocols/application-default-credentials
+    .. _Google Cloud SDK: https://cloud.google.com/sdk
+    .. _App Engine standard environment: https://cloud.google.com/appengine
+    .. _App Identity Service: https://cloud.google.com/appengine/docs/python\
+            /appidentity/
+    .. _Compute Engine: https://cloud.google.com/compute
+    .. _App Engine flexible environment: https://cloud.google.com\
+            /appengine/flexible
+    .. _Metadata Service: https://cloud.google.com/compute/docs\
+            /storing-retrieving-metadata
+    .. _Cloud Run: https://cloud.google.com/run
+
+    Example::
+
+        import google.auth
+
+        credentials, project_id = google.auth.default()
+
+    Args:
+        scopes (Sequence[str]): The list of scopes for the credentials. If
+            specified, the credentials will automatically be scoped if
+            necessary.
+        request (Optional[google.auth.transport.Request]): An object used to make
+            HTTP requests. This is used to either detect whether the application
+            is running on Compute Engine or to determine the associated project
+            ID for a workload identity pool resource (external account
+            credentials). If not specified, then it will either use the standard
+            library http client to make requests for Compute Engine credentials
+            or a google.auth.transport.requests.Request client for external
+            account credentials.
+        quota_project_id (Optional[str]): The project ID used for
+            quota and billing.
+        default_scopes (Optional[Sequence[str]]): Default scopes passed by a
+            Google client library. Use 'scopes' for user-defined scopes.
+    Returns:
+        Tuple[~google.auth.credentials.Credentials, Optional[str]]:
+            the current environment's credentials and project ID. Project ID
+            may be None, which indicates that the Project ID could not be
+            ascertained from the environment.
+
+    Raises:
+        ~google.auth.exceptions.DefaultCredentialsError:
+            If no credentials were found, or if the credentials found were
+            invalid.
+    """
+    from google.auth.credentials import with_scopes_if_required
+
+    explicit_project_id = os.environ.get(
+        environment_vars.PROJECT, os.environ.get(environment_vars.LEGACY_PROJECT)
+    )
+
+    checkers = (
+        # Avoid passing scopes here to prevent passing scopes to user credentials.
+        # with_scopes_if_required() below will ensure scopes/default scopes are
+        # safely set on the returned credentials since requires_scopes will
+        # guard against setting scopes on user credentials.
+        lambda: _get_explicit_environ_credentials(quota_project_id=quota_project_id),
+        lambda: _get_gcloud_sdk_credentials(quota_project_id=quota_project_id),
+        _get_gae_credentials,
+        lambda: _get_gce_credentials(request),
+    )
+
+    for checker in checkers:
+        credentials, project_id = checker()
+        if credentials is not None:
+            credentials = with_scopes_if_required(
+                credentials, scopes, default_scopes=default_scopes
+            )
+
+            # For external account credentials, scopes are required to determine
+            # the project ID. Try to get the project ID again if not yet
+            # determined.
+            if not project_id and callable(
+                getattr(credentials, "get_project_id", None)
+            ):
+                if request is None:
+                    request = google.auth.transport.requests.Request()
+                project_id = credentials.get_project_id(request=request)
+
+            if quota_project_id:
+                credentials = credentials.with_quota_project(quota_project_id)
+
+            effective_project_id = explicit_project_id or project_id
+            if not effective_project_id:
+                _LOGGER.warning(
+                    "No project ID could be determined. Consider running "
+                    "`gcloud config set project` or setting the %s "
+                    "environment variable",
+                    environment_vars.PROJECT,
+                )
+            return credentials, effective_project_id
+
+    raise exceptions.DefaultCredentialsError(_HELP_MESSAGE)
diff --git a/google/auth/_default_async.py b/google/auth/_default_async.py
new file mode 100644
index 0000000..fb277c5
--- /dev/null
+++ b/google/auth/_default_async.py
@@ -0,0 +1,281 @@
+# Copyright 2020 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Application default credentials.
+
+Implements application default credentials and project ID detection.
+"""
+
+import io
+import json
+import os
+
+import six
+
+from google.auth import _default
+from google.auth import environment_vars
+from google.auth import exceptions
+
+
+def load_credentials_from_file(filename, scopes=None, quota_project_id=None):
+    """Loads Google credentials from a file.
+
+    The credentials file must be a service account key or stored authorized
+    user credentials.
+
+    Args:
+        filename (str): The full path to the credentials file.
+        scopes (Optional[Sequence[str]]): The list of scopes for the credentials. If
+            specified, the credentials will automatically be scoped if
+            necessary
+        quota_project_id (Optional[str]):  The project ID used for
+                quota and billing.
+
+    Returns:
+        Tuple[google.auth.credentials.Credentials, Optional[str]]: Loaded
+            credentials and the project ID. Authorized user credentials do not
+            have the project ID information.
+
+    Raises:
+        google.auth.exceptions.DefaultCredentialsError: if the file is in the
+            wrong format or is missing.
+    """
+    if not os.path.exists(filename):
+        raise exceptions.DefaultCredentialsError(
+            "File {} was not found.".format(filename)
+        )
+
+    with io.open(filename, "r") as file_obj:
+        try:
+            info = json.load(file_obj)
+        except ValueError as caught_exc:
+            new_exc = exceptions.DefaultCredentialsError(
+                "File {} is not a valid json file.".format(filename), caught_exc
+            )
+            six.raise_from(new_exc, caught_exc)
+
+    # The type key should indicate that the file is either a service account
+    # credentials file or an authorized user credentials file.
+    credential_type = info.get("type")
+
+    if credential_type == _default._AUTHORIZED_USER_TYPE:
+        from google.oauth2 import _credentials_async as credentials
+
+        try:
+            credentials = credentials.Credentials.from_authorized_user_info(
+                info, scopes=scopes
+            )
+        except ValueError as caught_exc:
+            msg = "Failed to load authorized user credentials from {}".format(filename)
+            new_exc = exceptions.DefaultCredentialsError(msg, caught_exc)
+            six.raise_from(new_exc, caught_exc)
+        if quota_project_id:
+            credentials = credentials.with_quota_project(quota_project_id)
+        if not credentials.quota_project_id:
+            _default._warn_about_problematic_credentials(credentials)
+        return credentials, None
+
+    elif credential_type == _default._SERVICE_ACCOUNT_TYPE:
+        from google.oauth2 import _service_account_async as service_account
+
+        try:
+            credentials = service_account.Credentials.from_service_account_info(
+                info, scopes=scopes
+            ).with_quota_project(quota_project_id)
+        except ValueError as caught_exc:
+            msg = "Failed to load service account credentials from {}".format(filename)
+            new_exc = exceptions.DefaultCredentialsError(msg, caught_exc)
+            six.raise_from(new_exc, caught_exc)
+        return credentials, info.get("project_id")
+
+    else:
+        raise exceptions.DefaultCredentialsError(
+            "The file {file} does not have a valid type. "
+            "Type is {type}, expected one of {valid_types}.".format(
+                file=filename, type=credential_type, valid_types=_default._VALID_TYPES
+            )
+        )
+
+
+def _get_gcloud_sdk_credentials(quota_project_id=None):
+    """Gets the credentials and project ID from the Cloud SDK."""
+    from google.auth import _cloud_sdk
+
+    # Check if application default credentials exist.
+    credentials_filename = _cloud_sdk.get_application_default_credentials_path()
+
+    if not os.path.isfile(credentials_filename):
+        return None, None
+
+    credentials, project_id = load_credentials_from_file(
+        credentials_filename, quota_project_id=quota_project_id
+    )
+
+    if not project_id:
+        project_id = _cloud_sdk.get_project_id()
+
+    return credentials, project_id
+
+
+def _get_explicit_environ_credentials(quota_project_id=None):
+    """Gets credentials from the GOOGLE_APPLICATION_CREDENTIALS environment
+    variable."""
+    from google.auth import _cloud_sdk
+
+    cloud_sdk_adc_path = _cloud_sdk.get_application_default_credentials_path()
+    explicit_file = os.environ.get(environment_vars.CREDENTIALS)
+
+    if explicit_file is not None and explicit_file == cloud_sdk_adc_path:
+        # Cloud sdk flow calls gcloud to fetch project id, so if the explicit
+        # file path is cloud sdk credentials path, then we should fall back
+        # to cloud sdk flow, otherwise project id cannot be obtained.
+        return _get_gcloud_sdk_credentials(quota_project_id=quota_project_id)
+
+    if explicit_file is not None:
+        credentials, project_id = load_credentials_from_file(
+            os.environ[environment_vars.CREDENTIALS], quota_project_id=quota_project_id
+        )
+
+        return credentials, project_id
+
+    else:
+        return None, None
+
+
+def _get_gae_credentials():
+    """Gets Google App Engine App Identity credentials and project ID."""
+    # While this library is normally bundled with app_engine, there are
+    # some cases where it's not available, so we tolerate ImportError.
+
+    return _default._get_gae_credentials()
+
+
+def _get_gce_credentials(request=None):
+    """Gets credentials and project ID from the GCE Metadata Service."""
+    # Ping requires a transport, but we want application default credentials
+    # to require no arguments. So, we'll use the _http_client transport which
+    # uses http.client. This is only acceptable because the metadata server
+    # doesn't do SSL and never requires proxies.
+
+    # While this library is normally bundled with compute_engine, there are
+    # some cases where it's not available, so we tolerate ImportError.
+
+    return _default._get_gce_credentials(request)
+
+
+def default_async(scopes=None, request=None, quota_project_id=None):
+    """Gets the default credentials for the current environment.
+
+    `Application Default Credentials`_ provides an easy way to obtain
+    credentials to call Google APIs for server-to-server or local applications.
+    This function acquires credentials from the environment in the following
+    order:
+
+    1. If the environment variable ``GOOGLE_APPLICATION_CREDENTIALS`` is set
+       to the path of a valid service account JSON private key file, then it is
+       loaded and returned. The project ID returned is the project ID defined
+       in the service account file if available (some older files do not
+       contain project ID information).
+    2. If the `Google Cloud SDK`_ is installed and has application default
+       credentials set they are loaded and returned.
+
+       To enable application default credentials with the Cloud SDK run::
+
+            gcloud auth application-default login
+
+       If the Cloud SDK has an active project, the project ID is returned. The
+       active project can be set using::
+
+            gcloud config set project
+
+    3. If the application is running in the `App Engine standard environment`_
+       (first generation) then the credentials and project ID from the
+       `App Identity Service`_ are used.
+    4. If the application is running in `Compute Engine`_ or `Cloud Run`_ or
+       the `App Engine flexible environment`_ or the `App Engine standard
+       environment`_ (second generation) then the credentials and project ID
+       are obtained from the `Metadata Service`_.
+    5. If no credentials are found,
+       :class:`~google.auth.exceptions.DefaultCredentialsError` will be raised.
+
+    .. _Application Default Credentials: https://developers.google.com\
+            /identity/protocols/application-default-credentials
+    .. _Google Cloud SDK: https://cloud.google.com/sdk
+    .. _App Engine standard environment: https://cloud.google.com/appengine
+    .. _App Identity Service: https://cloud.google.com/appengine/docs/python\
+            /appidentity/
+    .. _Compute Engine: https://cloud.google.com/compute
+    .. _App Engine flexible environment: https://cloud.google.com\
+            /appengine/flexible
+    .. _Metadata Service: https://cloud.google.com/compute/docs\
+            /storing-retrieving-metadata
+    .. _Cloud Run: https://cloud.google.com/run
+
+    Example::
+
+        import google.auth
+
+        credentials, project_id = google.auth.default()
+
+    Args:
+        scopes (Sequence[str]): The list of scopes for the credentials. If
+            specified, the credentials will automatically be scoped if
+            necessary.
+        request (google.auth.transport.Request): An object used to make
+            HTTP requests. This is used to detect whether the application
+            is running on Compute Engine. If not specified, then it will
+            use the standard library http client to make requests.
+        quota_project_id (Optional[str]):  The project ID used for
+            quota and billing.
+    Returns:
+        Tuple[~google.auth.credentials.Credentials, Optional[str]]:
+            the current environment's credentials and project ID. Project ID
+            may be None, which indicates that the Project ID could not be
+            ascertained from the environment.
+
+    Raises:
+        ~google.auth.exceptions.DefaultCredentialsError:
+            If no credentials were found, or if the credentials found were
+            invalid.
+    """
+    from google.auth._credentials_async import with_scopes_if_required
+
+    explicit_project_id = os.environ.get(
+        environment_vars.PROJECT, os.environ.get(environment_vars.LEGACY_PROJECT)
+    )
+
+    checkers = (
+        lambda: _get_explicit_environ_credentials(quota_project_id=quota_project_id),
+        lambda: _get_gcloud_sdk_credentials(quota_project_id=quota_project_id),
+        _get_gae_credentials,
+        lambda: _get_gce_credentials(request),
+    )
+
+    for checker in checkers:
+        credentials, project_id = checker()
+        if credentials is not None:
+            credentials = with_scopes_if_required(
+                credentials, scopes
+            ).with_quota_project(quota_project_id)
+            effective_project_id = explicit_project_id or project_id
+            if not effective_project_id:
+                _default._LOGGER.warning(
+                    "No project ID could be determined. Consider running "
+                    "`gcloud config set project` or setting the %s "
+                    "environment variable",
+                    environment_vars.PROJECT,
+                )
+            return credentials, effective_project_id
+
+    raise exceptions.DefaultCredentialsError(_default._HELP_MESSAGE)
diff --git a/google/auth/_helpers.py b/google/auth/_helpers.py
new file mode 100644
index 0000000..1b08ab8
--- /dev/null
+++ b/google/auth/_helpers.py
@@ -0,0 +1,245 @@
+# Copyright 2015 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Helper functions for commonly used utilities."""
+
+import base64
+import calendar
+import datetime
+import sys
+
+import six
+from six.moves import urllib
+
+
+# Token server doesn't provide a new a token when doing refresh unless the
+# token is expiring within 30 seconds, so refresh threshold should not be
+# more than 30 seconds. Otherwise auth lib will send tons of refresh requests
+# until 30 seconds before the expiration, and cause a spike of CPU usage.
+REFRESH_THRESHOLD = datetime.timedelta(seconds=20)
+
+
+def copy_docstring(source_class):
+    """Decorator that copies a method's docstring from another class.
+
+    Args:
+        source_class (type): The class that has the documented method.
+
+    Returns:
+        Callable: A decorator that will copy the docstring of the same
+            named method in the source class to the decorated method.
+    """
+
+    def decorator(method):
+        """Decorator implementation.
+
+        Args:
+            method (Callable): The method to copy the docstring to.
+
+        Returns:
+            Callable: the same method passed in with an updated docstring.
+
+        Raises:
+            ValueError: if the method already has a docstring.
+        """
+        if method.__doc__:
+            raise ValueError("Method already has a docstring.")
+
+        source_method = getattr(source_class, method.__name__)
+        method.__doc__ = source_method.__doc__
+
+        return method
+
+    return decorator
+
+
+def utcnow():
+    """Returns the current UTC datetime.
+
+    Returns:
+        datetime: The current time in UTC.
+    """
+    return datetime.datetime.utcnow()
+
+
+def datetime_to_secs(value):
+    """Convert a datetime object to the number of seconds since the UNIX epoch.
+
+    Args:
+        value (datetime): The datetime to convert.
+
+    Returns:
+        int: The number of seconds since the UNIX epoch.
+    """
+    return calendar.timegm(value.utctimetuple())
+
+
+def to_bytes(value, encoding="utf-8"):
+    """Converts a string value to bytes, if necessary.
+
+    Unfortunately, ``six.b`` is insufficient for this task since in
+    Python 2 because it does not modify ``unicode`` objects.
+
+    Args:
+        value (Union[str, bytes]): The value to be converted.
+        encoding (str): The encoding to use to convert unicode to bytes.
+            Defaults to "utf-8".
+
+    Returns:
+        bytes: The original value converted to bytes (if unicode) or as
+            passed in if it started out as bytes.
+
+    Raises:
+        ValueError: If the value could not be converted to bytes.
+    """
+    result = value.encode(encoding) if isinstance(value, six.text_type) else value
+    if isinstance(result, six.binary_type):
+        return result
+    else:
+        raise ValueError("{0!r} could not be converted to bytes".format(value))
+
+
+def from_bytes(value):
+    """Converts bytes to a string value, if necessary.
+
+    Args:
+        value (Union[str, bytes]): The value to be converted.
+
+    Returns:
+        str: The original value converted to unicode (if bytes) or as passed in
+            if it started out as unicode.
+
+    Raises:
+        ValueError: If the value could not be converted to unicode.
+    """
+    result = value.decode("utf-8") if isinstance(value, six.binary_type) else value
+    if isinstance(result, six.text_type):
+        return result
+    else:
+        raise ValueError("{0!r} could not be converted to unicode".format(value))
+
+
+def update_query(url, params, remove=None):
+    """Updates a URL's query parameters.
+
+    Replaces any current values if they are already present in the URL.
+
+    Args:
+        url (str): The URL to update.
+        params (Mapping[str, str]): A mapping of query parameter
+            keys to values.
+        remove (Sequence[str]): Parameters to remove from the query string.
+
+    Returns:
+        str: The URL with updated query parameters.
+
+    Examples:
+
+        >>> url = 'http://example.com?a=1'
+        >>> update_query(url, {'a': '2'})
+        http://example.com?a=2
+        >>> update_query(url, {'b': '3'})
+        http://example.com?a=1&b=3
+        >> update_query(url, {'b': '3'}, remove=['a'])
+        http://example.com?b=3
+
+    """
+    if remove is None:
+        remove = []
+
+    # Split the URL into parts.
+    parts = urllib.parse.urlparse(url)
+    # Parse the query string.
+    query_params = urllib.parse.parse_qs(parts.query)
+    # Update the query parameters with the new parameters.
+    query_params.update(params)
+    # Remove any values specified in remove.
+    query_params = {
+        key: value for key, value in six.iteritems(query_params) if key not in remove
+    }
+    # Re-encoded the query string.
+    new_query = urllib.parse.urlencode(query_params, doseq=True)
+    # Unsplit the url.
+    new_parts = parts._replace(query=new_query)
+    return urllib.parse.urlunparse(new_parts)
+
+
+def scopes_to_string(scopes):
+    """Converts scope value to a string suitable for sending to OAuth 2.0
+    authorization servers.
+
+    Args:
+        scopes (Sequence[str]): The sequence of scopes to convert.
+
+    Returns:
+        str: The scopes formatted as a single string.
+    """
+    return " ".join(scopes)
+
+
+def string_to_scopes(scopes):
+    """Converts stringifed scopes value to a list.
+
+    Args:
+        scopes (Union[Sequence, str]): The string of space-separated scopes
+            to convert.
+    Returns:
+        Sequence(str): The separated scopes.
+    """
+    if not scopes:
+        return []
+
+    return scopes.split(" ")
+
+
+def padded_urlsafe_b64decode(value):
+    """Decodes base64 strings lacking padding characters.
+
+    Google infrastructure tends to omit the base64 padding characters.
+
+    Args:
+        value (Union[str, bytes]): The encoded value.
+
+    Returns:
+        bytes: The decoded value
+    """
+    b64string = to_bytes(value)
+    padded = b64string + b"=" * (-len(b64string) % 4)
+    return base64.urlsafe_b64decode(padded)
+
+
+def unpadded_urlsafe_b64encode(value):
+    """Encodes base64 strings removing any padding characters.
+
+    `rfc 7515`_ defines Base64url to NOT include any padding
+    characters, but the stdlib doesn't do that by default.
+
+    _rfc7515: https://tools.ietf.org/html/rfc7515#page-6
+
+    Args:
+        value (Union[str|bytes]): The bytes-like value to encode
+
+    Returns:
+        Union[str|bytes]: The encoded value
+    """
+    return base64.urlsafe_b64encode(value).rstrip(b"=")
+
+
+def is_python_3():
+    """Check if the Python interpreter is Python 2 or 3.
+
+    Returns:
+        bool: True if the Python interpreter is Python 3 and False otherwise.
+    """
+    return sys.version_info > (3, 0)
diff --git a/google/auth/_jwt_async.py b/google/auth/_jwt_async.py
new file mode 100644
index 0000000..49e3026
--- /dev/null
+++ b/google/auth/_jwt_async.py
@@ -0,0 +1,168 @@
+# 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.
+
+"""JSON Web Tokens
+
+Provides support for creating (encoding) and verifying (decoding) JWTs,
+especially JWTs generated and consumed by Google infrastructure.
+
+See `rfc7519`_ for more details on JWTs.
+
+To encode a JWT use :func:`encode`::
+
+    from google.auth import crypt
+    from google.auth import jwt_async
+
+    signer = crypt.Signer(private_key)
+    payload = {'some': 'payload'}
+    encoded = jwt_async.encode(signer, payload)
+
+To decode a JWT and verify claims use :func:`decode`::
+
+    claims = jwt_async.decode(encoded, certs=public_certs)
+
+You can also skip verification::
+
+    claims = jwt_async.decode(encoded, verify=False)
+
+.. _rfc7519: https://tools.ietf.org/html/rfc7519
+
+
+NOTE: This async support is experimental and marked internal. This surface may
+change in minor releases.
+"""
+
+import google.auth
+from google.auth import jwt
+
+
+def encode(signer, payload, header=None, key_id=None):
+    """Make a signed JWT.
+
+    Args:
+        signer (google.auth.crypt.Signer): The signer used to sign the JWT.
+        payload (Mapping[str, str]): The JWT payload.
+        header (Mapping[str, str]): Additional JWT header payload.
+        key_id (str): The key id to add to the JWT header. If the
+            signer has a key id it will be used as the default. If this is
+            specified it will override the signer's key id.
+
+    Returns:
+        bytes: The encoded JWT.
+    """
+    return jwt.encode(signer, payload, header, key_id)
+
+
+def decode(token, certs=None, verify=True, audience=None):
+    """Decode and verify a JWT.
+
+    Args:
+        token (str): The encoded JWT.
+        certs (Union[str, bytes, Mapping[str, Union[str, bytes]]]): The
+            certificate used to validate the JWT signature. If bytes or string,
+            it must the the public key certificate in PEM format. If a mapping,
+            it must be a mapping of key IDs to public key certificates in PEM
+            format. The mapping must contain the same key ID that's specified
+            in the token's header.
+        verify (bool): Whether to perform signature and claim validation.
+            Verification is done by default.
+        audience (str): The audience claim, 'aud', that this JWT should
+            contain. If None then the JWT's 'aud' parameter is not verified.
+
+    Returns:
+        Mapping[str, str]: The deserialized JSON payload in the JWT.
+
+    Raises:
+        ValueError: if any verification checks failed.
+    """
+
+    return jwt.decode(token, certs, verify, audience)
+
+
+class Credentials(
+    jwt.Credentials,
+    google.auth._credentials_async.Signing,
+    google.auth._credentials_async.Credentials,
+):
+    """Credentials that use a JWT as the bearer token.
+
+    These credentials require an "audience" claim. This claim identifies the
+    intended recipient of the bearer token.
+
+    The constructor arguments determine the claims for the JWT that is
+    sent with requests. Usually, you'll construct these credentials with
+    one of the helper constructors as shown in the next section.
+
+    To create JWT credentials using a Google service account private key
+    JSON file::
+
+        audience = 'https://pubsub.googleapis.com/google.pubsub.v1.Publisher'
+        credentials = jwt_async.Credentials.from_service_account_file(
+            'service-account.json',
+            audience=audience)
+
+    If you already have the service account file loaded and parsed::
+
+        service_account_info = json.load(open('service_account.json'))
+        credentials = jwt_async.Credentials.from_service_account_info(
+            service_account_info,
+            audience=audience)
+
+    Both helper methods pass on arguments to the constructor, so you can
+    specify the JWT claims::
+
+        credentials = jwt_async.Credentials.from_service_account_file(
+            'service-account.json',
+            audience=audience,
+            additional_claims={'meta': 'data'})
+
+    You can also construct the credentials directly if you have a
+    :class:`~google.auth.crypt.Signer` instance::
+
+        credentials = jwt_async.Credentials(
+            signer,
+            issuer='your-issuer',
+            subject='your-subject',
+            audience=audience)
+
+    The claims are considered immutable. If you want to modify the claims,
+    you can easily create another instance using :meth:`with_claims`::
+
+        new_audience = (
+            'https://pubsub.googleapis.com/google.pubsub.v1.Subscriber')
+        new_credentials = credentials.with_claims(audience=new_audience)
+    """
+
+
+class OnDemandCredentials(
+    jwt.OnDemandCredentials,
+    google.auth._credentials_async.Signing,
+    google.auth._credentials_async.Credentials,
+):
+    """On-demand JWT credentials.
+
+    Like :class:`Credentials`, this class uses a JWT as the bearer token for
+    authentication. However, this class does not require the audience at
+    construction time. Instead, it will generate a new token on-demand for
+    each request using the request URI as the audience. It caches tokens
+    so that multiple requests to the same URI do not incur the overhead
+    of generating a new token every time.
+
+    This behavior is especially useful for `gRPC`_ clients. A gRPC service may
+    have multiple audience and gRPC clients may not know all of the audiences
+    required for accessing a particular service. With these credentials,
+    no knowledge of the audiences is required ahead of time.
+
+    .. _grpc: http://www.grpc.io/
+    """
diff --git a/google/auth/_oauth2client.py b/google/auth/_oauth2client.py
new file mode 100644
index 0000000..95a9876
--- /dev/null
+++ b/google/auth/_oauth2client.py
@@ -0,0 +1,169 @@
+# Copyright 2016 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.
+
+"""Helpers for transitioning from oauth2client to google-auth.
+
+.. warning::
+    This module is private as it is intended to assist first-party downstream
+    clients with the transition from oauth2client to google-auth.
+"""
+
+from __future__ import absolute_import
+
+import six
+
+from google.auth import _helpers
+import google.auth.app_engine
+import google.auth.compute_engine
+import google.oauth2.credentials
+import google.oauth2.service_account
+
+try:
+    import oauth2client.client
+    import oauth2client.contrib.gce
+    import oauth2client.service_account
+except ImportError as caught_exc:
+    six.raise_from(ImportError("oauth2client is not installed."), caught_exc)
+
+try:
+    import oauth2client.contrib.appengine  # pytype: disable=import-error
+
+    _HAS_APPENGINE = True
+except ImportError:
+    _HAS_APPENGINE = False
+
+
+_CONVERT_ERROR_TMPL = "Unable to convert {} to a google-auth credentials class."
+
+
+def _convert_oauth2_credentials(credentials):
+    """Converts to :class:`google.oauth2.credentials.Credentials`.
+
+    Args:
+        credentials (Union[oauth2client.client.OAuth2Credentials,
+            oauth2client.client.GoogleCredentials]): The credentials to
+            convert.
+
+    Returns:
+        google.oauth2.credentials.Credentials: The converted credentials.
+    """
+    new_credentials = google.oauth2.credentials.Credentials(
+        token=credentials.access_token,
+        refresh_token=credentials.refresh_token,
+        token_uri=credentials.token_uri,
+        client_id=credentials.client_id,
+        client_secret=credentials.client_secret,
+        scopes=credentials.scopes,
+    )
+
+    new_credentials._expires = credentials.token_expiry
+
+    return new_credentials
+
+
+def _convert_service_account_credentials(credentials):
+    """Converts to :class:`google.oauth2.service_account.Credentials`.
+
+    Args:
+        credentials (Union[
+            oauth2client.service_account.ServiceAccountCredentials,
+            oauth2client.service_account._JWTAccessCredentials]): The
+            credentials to convert.
+
+    Returns:
+        google.oauth2.service_account.Credentials: The converted credentials.
+    """
+    info = credentials.serialization_data.copy()
+    info["token_uri"] = credentials.token_uri
+    return google.oauth2.service_account.Credentials.from_service_account_info(info)
+
+
+def _convert_gce_app_assertion_credentials(credentials):
+    """Converts to :class:`google.auth.compute_engine.Credentials`.
+
+    Args:
+        credentials (oauth2client.contrib.gce.AppAssertionCredentials): The
+            credentials to convert.
+
+    Returns:
+        google.oauth2.service_account.Credentials: The converted credentials.
+    """
+    return google.auth.compute_engine.Credentials(
+        service_account_email=credentials.service_account_email
+    )
+
+
+def _convert_appengine_app_assertion_credentials(credentials):
+    """Converts to :class:`google.auth.app_engine.Credentials`.
+
+    Args:
+        credentials (oauth2client.contrib.app_engine.AppAssertionCredentials):
+            The credentials to convert.
+
+    Returns:
+        google.oauth2.service_account.Credentials: The converted credentials.
+    """
+    # pylint: disable=invalid-name
+    return google.auth.app_engine.Credentials(
+        scopes=_helpers.string_to_scopes(credentials.scope),
+        service_account_id=credentials.service_account_id,
+    )
+
+
+_CLASS_CONVERSION_MAP = {
+    oauth2client.client.OAuth2Credentials: _convert_oauth2_credentials,
+    oauth2client.client.GoogleCredentials: _convert_oauth2_credentials,
+    oauth2client.service_account.ServiceAccountCredentials: _convert_service_account_credentials,
+    oauth2client.service_account._JWTAccessCredentials: _convert_service_account_credentials,
+    oauth2client.contrib.gce.AppAssertionCredentials: _convert_gce_app_assertion_credentials,
+}
+
+if _HAS_APPENGINE:
+    _CLASS_CONVERSION_MAP[
+        oauth2client.contrib.appengine.AppAssertionCredentials
+    ] = _convert_appengine_app_assertion_credentials
+
+
+def convert(credentials):
+    """Convert oauth2client credentials to google-auth credentials.
+
+    This class converts:
+
+    - :class:`oauth2client.client.OAuth2Credentials` to
+      :class:`google.oauth2.credentials.Credentials`.
+    - :class:`oauth2client.client.GoogleCredentials` to
+      :class:`google.oauth2.credentials.Credentials`.
+    - :class:`oauth2client.service_account.ServiceAccountCredentials` to
+      :class:`google.oauth2.service_account.Credentials`.
+    - :class:`oauth2client.service_account._JWTAccessCredentials` to
+      :class:`google.oauth2.service_account.Credentials`.
+    - :class:`oauth2client.contrib.gce.AppAssertionCredentials` to
+      :class:`google.auth.compute_engine.Credentials`.
+    - :class:`oauth2client.contrib.appengine.AppAssertionCredentials` to
+      :class:`google.auth.app_engine.Credentials`.
+
+    Returns:
+        google.auth.credentials.Credentials: The converted credentials.
+
+    Raises:
+        ValueError: If the credentials could not be converted.
+    """
+
+    credentials_class = type(credentials)
+
+    try:
+        return _CLASS_CONVERSION_MAP[credentials_class](credentials)
+    except KeyError as caught_exc:
+        new_exc = ValueError(_CONVERT_ERROR_TMPL.format(credentials_class))
+        six.raise_from(new_exc, caught_exc)
diff --git a/google/auth/_service_account_info.py b/google/auth/_service_account_info.py
new file mode 100644
index 0000000..3d340c7
--- /dev/null
+++ b/google/auth/_service_account_info.py
@@ -0,0 +1,74 @@
+# Copyright 2016 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.
+
+"""Helper functions for loading data from a Google service account file."""
+
+import io
+import json
+
+import six
+
+from google.auth import crypt
+
+
+def from_dict(data, require=None):
+    """Validates a dictionary containing Google service account data.
+
+    Creates and returns a :class:`google.auth.crypt.Signer` instance from the
+    private key specified in the data.
+
+    Args:
+        data (Mapping[str, str]): The service account data
+        require (Sequence[str]): List of keys required to be present in the
+            info.
+
+    Returns:
+        google.auth.crypt.Signer: A signer created from the private key in the
+            service account file.
+
+    Raises:
+        ValueError: if the data was in the wrong format, or if one of the
+            required keys is missing.
+    """
+    keys_needed = set(require if require is not None else [])
+
+    missing = keys_needed.difference(six.iterkeys(data))
+
+    if missing:
+        raise ValueError(
+            "Service account info was not in the expected format, missing "
+            "fields {}.".format(", ".join(missing))
+        )
+
+    # Create a signer.
+    signer = crypt.RSASigner.from_service_account_info(data)
+
+    return signer
+
+
+def from_filename(filename, require=None):
+    """Reads a Google service account JSON file and returns its parsed info.
+
+    Args:
+        filename (str): The path to the service account .json file.
+        require (Sequence[str]): List of keys required to be present in the
+            info.
+
+    Returns:
+        Tuple[ Mapping[str, str], google.auth.crypt.Signer ]: The verified
+            info and a signer instance.
+    """
+    with io.open(filename, "r", encoding="utf-8") as json_file:
+        data = json.load(json_file)
+        return data, from_dict(data, require=require)
diff --git a/google/auth/app_engine.py b/google/auth/app_engine.py
new file mode 100644
index 0000000..81aef73
--- /dev/null
+++ b/google/auth/app_engine.py
@@ -0,0 +1,179 @@
+# Copyright 2016 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.
+
+"""Google App Engine standard environment support.
+
+This module provides authentication and signing for applications running on App
+Engine in the standard environment using the `App Identity API`_.
+
+
+.. _App Identity API:
+    https://cloud.google.com/appengine/docs/python/appidentity/
+"""
+
+import datetime
+
+from google.auth import _helpers
+from google.auth import credentials
+from google.auth import crypt
+
+# pytype: disable=import-error
+try:
+    from google.appengine.api import app_identity
+except ImportError:
+    app_identity = None
+# pytype: enable=import-error
+
+
+class Signer(crypt.Signer):
+    """Signs messages using the App Engine App Identity service.
+
+    This can be used in place of :class:`google.auth.crypt.Signer` when
+    running in the App Engine standard environment.
+    """
+
+    @property
+    def key_id(self):
+        """Optional[str]: The key ID used to identify this private key.
+
+        .. warning::
+           This is always ``None``. The key ID used by App Engine can not
+           be reliably determined ahead of time.
+        """
+        return None
+
+    @_helpers.copy_docstring(crypt.Signer)
+    def sign(self, message):
+        message = _helpers.to_bytes(message)
+        _, signature = app_identity.sign_blob(message)
+        return signature
+
+
+def get_project_id():
+    """Gets the project ID for the current App Engine application.
+
+    Returns:
+        str: The project ID
+
+    Raises:
+        EnvironmentError: If the App Engine APIs are unavailable.
+    """
+    # pylint: disable=missing-raises-doc
+    # Pylint rightfully thinks EnvironmentError is OSError, but doesn't
+    # realize it's a valid alias.
+    if app_identity is None:
+        raise EnvironmentError("The App Engine APIs are not available.")
+    return app_identity.get_application_id()
+
+
+class Credentials(
+    credentials.Scoped, credentials.Signing, credentials.CredentialsWithQuotaProject
+):
+    """App Engine standard environment credentials.
+
+    These credentials use the App Engine App Identity API to obtain access
+    tokens.
+    """
+
+    def __init__(
+        self,
+        scopes=None,
+        default_scopes=None,
+        service_account_id=None,
+        quota_project_id=None,
+    ):
+        """
+        Args:
+            scopes (Sequence[str]): Scopes to request from the App Identity
+                API.
+            default_scopes (Sequence[str]): Default scopes passed by a
+                Google client library. Use 'scopes' for user-defined scopes.
+            service_account_id (str): The service account ID passed into
+                :func:`google.appengine.api.app_identity.get_access_token`.
+                If not specified, the default application service account
+                ID will be used.
+            quota_project_id (Optional[str]): The project ID used for quota
+                and billing.
+
+        Raises:
+            EnvironmentError: If the App Engine APIs are unavailable.
+        """
+        # pylint: disable=missing-raises-doc
+        # Pylint rightfully thinks EnvironmentError is OSError, but doesn't
+        # realize it's a valid alias.
+        if app_identity is None:
+            raise EnvironmentError("The App Engine APIs are not available.")
+
+        super(Credentials, self).__init__()
+        self._scopes = scopes
+        self._default_scopes = default_scopes
+        self._service_account_id = service_account_id
+        self._signer = Signer()
+        self._quota_project_id = quota_project_id
+
+    @_helpers.copy_docstring(credentials.Credentials)
+    def refresh(self, request):
+        scopes = self._scopes if self._scopes is not None else self._default_scopes
+        # pylint: disable=unused-argument
+        token, ttl = app_identity.get_access_token(scopes, self._service_account_id)
+        expiry = datetime.datetime.utcfromtimestamp(ttl)
+
+        self.token, self.expiry = token, expiry
+
+    @property
+    def service_account_email(self):
+        """The service account email."""
+        if self._service_account_id is None:
+            self._service_account_id = app_identity.get_service_account_name()
+        return self._service_account_id
+
+    @property
+    def requires_scopes(self):
+        """Checks if the credentials requires scopes.
+
+        Returns:
+            bool: True if there are no scopes set otherwise False.
+        """
+        return not self._scopes and not self._default_scopes
+
+    @_helpers.copy_docstring(credentials.Scoped)
+    def with_scopes(self, scopes, default_scopes=None):
+        return self.__class__(
+            scopes=scopes,
+            default_scopes=default_scopes,
+            service_account_id=self._service_account_id,
+            quota_project_id=self.quota_project_id,
+        )
+
+    @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
+    def with_quota_project(self, quota_project_id):
+        return self.__class__(
+            scopes=self._scopes,
+            service_account_id=self._service_account_id,
+            quota_project_id=quota_project_id,
+        )
+
+    @_helpers.copy_docstring(credentials.Signing)
+    def sign_bytes(self, message):
+        return self._signer.sign(message)
+
+    @property
+    @_helpers.copy_docstring(credentials.Signing)
+    def signer_email(self):
+        return self.service_account_email
+
+    @property
+    @_helpers.copy_docstring(credentials.Signing)
+    def signer(self):
+        return self._signer
diff --git a/google/auth/aws.py b/google/auth/aws.py
new file mode 100644
index 0000000..925b1dd
--- /dev/null
+++ b/google/auth/aws.py
@@ -0,0 +1,731 @@
+# 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.
+
+"""AWS Credentials and AWS Signature V4 Request Signer.
+
+This module provides credentials to access Google Cloud resources from Amazon
+Web Services (AWS) workloads. These credentials are recommended over the
+use of service account credentials in AWS as they do not involve the management
+of long-live service account private keys.
+
+AWS Credentials are initialized using external_account arguments which are
+typically loaded from the external credentials JSON file.
+Unlike other Credentials that can be initialized with a list of explicit
+arguments, secrets or credentials, external account clients use the
+environment and hints/guidelines provided by the external_account JSON
+file to retrieve credentials and exchange them for Google access tokens.
+
+This module also provides a basic implementation of the
+`AWS Signature Version 4`_ request signing algorithm.
+
+AWS Credentials use serialized signed requests to the
+`AWS STS GetCallerIdentity`_ API that can be exchanged for Google access tokens
+via the GCP STS endpoint.
+
+.. _AWS Signature Version 4: https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
+.. _AWS STS GetCallerIdentity: https://docs.aws.amazon.com/STS/latest/APIReference/API_GetCallerIdentity.html
+"""
+
+import hashlib
+import hmac
+import io
+import json
+import os
+import posixpath
+import re
+
+try:
+    from urllib.parse import urljoin
+# Python 2.7 compatibility
+except ImportError:  # pragma: NO COVER
+    from urlparse import urljoin
+
+from six.moves import http_client
+from six.moves import urllib
+
+from google.auth import _helpers
+from google.auth import environment_vars
+from google.auth import exceptions
+from google.auth import external_account
+
+# AWS Signature Version 4 signing algorithm identifier.
+_AWS_ALGORITHM = "AWS4-HMAC-SHA256"
+# The termination string for the AWS credential scope value as defined in
+# https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
+_AWS_REQUEST_TYPE = "aws4_request"
+# The AWS authorization header name for the security session token if available.
+_AWS_SECURITY_TOKEN_HEADER = "x-amz-security-token"
+# The AWS authorization header name for the auto-generated date.
+_AWS_DATE_HEADER = "x-amz-date"
+
+
+class RequestSigner(object):
+    """Implements an AWS request signer based on the AWS Signature Version 4 signing
+    process.
+    https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
+    """
+
+    def __init__(self, region_name):
+        """Instantiates an AWS request signer used to compute authenticated signed
+        requests to AWS APIs based on the AWS Signature Version 4 signing process.
+
+        Args:
+            region_name (str): The AWS region to use.
+        """
+
+        self._region_name = region_name
+
+    def get_request_options(
+        self,
+        aws_security_credentials,
+        url,
+        method,
+        request_payload="",
+        additional_headers={},
+    ):
+        """Generates the signed request for the provided HTTP request for calling
+        an AWS API. This follows the steps described at:
+        https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html
+
+        Args:
+            aws_security_credentials (Mapping[str, str]): A dictionary containing
+                the AWS security credentials.
+            url (str): The AWS service URL containing the canonical URI and
+                query string.
+            method (str): The HTTP method used to call this API.
+            request_payload (Optional[str]): The optional request payload if
+                available.
+            additional_headers (Optional[Mapping[str, str]]): The optional
+                additional headers needed for the requested AWS API.
+
+        Returns:
+            Mapping[str, str]: The AWS signed request dictionary object.
+        """
+        # Get AWS credentials.
+        access_key = aws_security_credentials.get("access_key_id")
+        secret_key = aws_security_credentials.get("secret_access_key")
+        security_token = aws_security_credentials.get("security_token")
+
+        additional_headers = additional_headers or {}
+
+        uri = urllib.parse.urlparse(url)
+        # Normalize the URL path. This is needed for the canonical_uri.
+        # os.path.normpath can't be used since it normalizes "/" paths
+        # to "\\" in Windows OS.
+        normalized_uri = urllib.parse.urlparse(
+            urljoin(url, posixpath.normpath(uri.path))
+        )
+        # Validate provided URL.
+        if not uri.hostname or uri.scheme != "https":
+            raise ValueError("Invalid AWS service URL")
+
+        header_map = _generate_authentication_header_map(
+            host=uri.hostname,
+            canonical_uri=normalized_uri.path or "/",
+            canonical_querystring=_get_canonical_querystring(uri.query),
+            method=method,
+            region=self._region_name,
+            access_key=access_key,
+            secret_key=secret_key,
+            security_token=security_token,
+            request_payload=request_payload,
+            additional_headers=additional_headers,
+        )
+        headers = {
+            "Authorization": header_map.get("authorization_header"),
+            "host": uri.hostname,
+        }
+        # Add x-amz-date if available.
+        if "amz_date" in header_map:
+            headers[_AWS_DATE_HEADER] = header_map.get("amz_date")
+        # Append additional optional headers, eg. X-Amz-Target, Content-Type, etc.
+        for key in additional_headers:
+            headers[key] = additional_headers[key]
+
+        # Add session token if available.
+        if security_token is not None:
+            headers[_AWS_SECURITY_TOKEN_HEADER] = security_token
+
+        signed_request = {"url": url, "method": method, "headers": headers}
+        if request_payload:
+            signed_request["data"] = request_payload
+        return signed_request
+
+
+def _get_canonical_querystring(query):
+    """Generates the canonical query string given a raw query string.
+    Logic is based on
+    https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
+
+    Args:
+        query (str): The raw query string.
+
+    Returns:
+        str: The canonical query string.
+    """
+    # Parse raw query string.
+    querystring = urllib.parse.parse_qs(query)
+    querystring_encoded_map = {}
+    for key in querystring:
+        quote_key = urllib.parse.quote(key, safe="-_.~")
+        # URI encode key.
+        querystring_encoded_map[quote_key] = []
+        for item in querystring[key]:
+            # For each key, URI encode all values for that key.
+            querystring_encoded_map[quote_key].append(
+                urllib.parse.quote(item, safe="-_.~")
+            )
+        # Sort values for each key.
+        querystring_encoded_map[quote_key].sort()
+    # Sort keys.
+    sorted_keys = list(querystring_encoded_map.keys())
+    sorted_keys.sort()
+    # Reconstruct the query string. Preserve keys with multiple values.
+    querystring_encoded_pairs = []
+    for key in sorted_keys:
+        for item in querystring_encoded_map[key]:
+            querystring_encoded_pairs.append("{}={}".format(key, item))
+    return "&".join(querystring_encoded_pairs)
+
+
+def _sign(key, msg):
+    """Creates the HMAC-SHA256 hash of the provided message using the provided
+    key.
+
+    Args:
+        key (str): The HMAC-SHA256 key to use.
+        msg (str): The message to hash.
+
+    Returns:
+        str: The computed hash bytes.
+    """
+    return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()
+
+
+def _get_signing_key(key, date_stamp, region_name, service_name):
+    """Calculates the signing key used to calculate the signature for
+    AWS Signature Version 4 based on:
+    https://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
+
+    Args:
+        key (str): The AWS secret access key.
+        date_stamp (str): The '%Y%m%d' date format.
+        region_name (str): The AWS region.
+        service_name (str): The AWS service name, eg. sts.
+
+    Returns:
+        str: The signing key bytes.
+    """
+    k_date = _sign(("AWS4" + key).encode("utf-8"), date_stamp)
+    k_region = _sign(k_date, region_name)
+    k_service = _sign(k_region, service_name)
+    k_signing = _sign(k_service, "aws4_request")
+    return k_signing
+
+
+def _generate_authentication_header_map(
+    host,
+    canonical_uri,
+    canonical_querystring,
+    method,
+    region,
+    access_key,
+    secret_key,
+    security_token,
+    request_payload="",
+    additional_headers={},
+):
+    """Generates the authentication header map needed for generating the AWS
+    Signature Version 4 signed request.
+
+    Args:
+        host (str): The AWS service URL hostname.
+        canonical_uri (str): The AWS service URL path name.
+        canonical_querystring (str): The AWS service URL query string.
+        method (str): The HTTP method used to call this API.
+        region (str): The AWS region.
+        access_key (str): The AWS access key ID.
+        secret_key (str): The AWS secret access key.
+        security_token (Optional[str]): The AWS security session token. This is
+            available for temporary sessions.
+        request_payload (Optional[str]): The optional request payload if
+            available.
+        additional_headers (Optional[Mapping[str, str]]): The optional
+            additional headers needed for the requested AWS API.
+
+    Returns:
+        Mapping[str, str]: The AWS authentication header dictionary object.
+            This contains the x-amz-date and authorization header information.
+    """
+    # iam.amazonaws.com host => iam service.
+    # sts.us-east-2.amazonaws.com host => sts service.
+    service_name = host.split(".")[0]
+
+    current_time = _helpers.utcnow()
+    amz_date = current_time.strftime("%Y%m%dT%H%M%SZ")
+    date_stamp = current_time.strftime("%Y%m%d")
+
+    # Change all additional headers to be lower case.
+    full_headers = {}
+    for key in additional_headers:
+        full_headers[key.lower()] = additional_headers[key]
+    # Add AWS session token if available.
+    if security_token is not None:
+        full_headers[_AWS_SECURITY_TOKEN_HEADER] = security_token
+
+    # Required headers
+    full_headers["host"] = host
+    # Do not use generated x-amz-date if the date header is provided.
+    # Previously the date was not fixed with x-amz- and could be provided
+    # manually.
+    # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-header-value-trim.req
+    if "date" not in full_headers:
+        full_headers[_AWS_DATE_HEADER] = amz_date
+
+    # Header keys need to be sorted alphabetically.
+    canonical_headers = ""
+    header_keys = list(full_headers.keys())
+    header_keys.sort()
+    for key in header_keys:
+        canonical_headers = "{}{}:{}\n".format(
+            canonical_headers, key, full_headers[key]
+        )
+    signed_headers = ";".join(header_keys)
+
+    payload_hash = hashlib.sha256((request_payload or "").encode("utf-8")).hexdigest()
+
+    # https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
+    canonical_request = "{}\n{}\n{}\n{}\n{}\n{}".format(
+        method,
+        canonical_uri,
+        canonical_querystring,
+        canonical_headers,
+        signed_headers,
+        payload_hash,
+    )
+
+    credential_scope = "{}/{}/{}/{}".format(
+        date_stamp, region, service_name, _AWS_REQUEST_TYPE
+    )
+
+    # https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
+    string_to_sign = "{}\n{}\n{}\n{}".format(
+        _AWS_ALGORITHM,
+        amz_date,
+        credential_scope,
+        hashlib.sha256(canonical_request.encode("utf-8")).hexdigest(),
+    )
+
+    # https://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
+    signing_key = _get_signing_key(secret_key, date_stamp, region, service_name)
+    signature = hmac.new(
+        signing_key, string_to_sign.encode("utf-8"), hashlib.sha256
+    ).hexdigest()
+
+    # https://docs.aws.amazon.com/general/latest/gr/sigv4-add-signature-to-request.html
+    authorization_header = "{} Credential={}/{}, SignedHeaders={}, Signature={}".format(
+        _AWS_ALGORITHM, access_key, credential_scope, signed_headers, signature
+    )
+
+    authentication_header = {"authorization_header": authorization_header}
+    # Do not use generated x-amz-date if the date header is provided.
+    if "date" not in full_headers:
+        authentication_header["amz_date"] = amz_date
+    return authentication_header
+
+
+class Credentials(external_account.Credentials):
+    """AWS external account credentials.
+    This is used to exchange serialized AWS signature v4 signed requests to
+    AWS STS GetCallerIdentity service for Google access tokens.
+    """
+
+    def __init__(
+        self,
+        audience,
+        subject_token_type,
+        token_url,
+        credential_source=None,
+        service_account_impersonation_url=None,
+        client_id=None,
+        client_secret=None,
+        quota_project_id=None,
+        scopes=None,
+        default_scopes=None,
+    ):
+        """Instantiates an AWS workload external account credentials object.
+
+        Args:
+            audience (str): The STS audience field.
+            subject_token_type (str): The subject token type.
+            token_url (str): The STS endpoint URL.
+            credential_source (Mapping): The credential source dictionary used
+                to provide instructions on how to retrieve external credential
+                to be exchanged for Google access tokens.
+            service_account_impersonation_url (Optional[str]): The optional
+                service account impersonation getAccessToken URL.
+            client_id (Optional[str]): The optional client ID.
+            client_secret (Optional[str]): The optional client secret.
+            quota_project_id (Optional[str]): The optional quota project ID.
+            scopes (Optional[Sequence[str]]): Optional scopes to request during
+                the authorization grant.
+            default_scopes (Optional[Sequence[str]]): Default scopes passed by a
+                Google client library. Use 'scopes' for user-defined scopes.
+
+        Raises:
+            google.auth.exceptions.RefreshError: If an error is encountered during
+                access token retrieval logic.
+            ValueError: For invalid parameters.
+
+        .. note:: Typically one of the helper constructors
+            :meth:`from_file` or
+            :meth:`from_info` are used instead of calling the constructor directly.
+        """
+        super(Credentials, self).__init__(
+            audience=audience,
+            subject_token_type=subject_token_type,
+            token_url=token_url,
+            credential_source=credential_source,
+            service_account_impersonation_url=service_account_impersonation_url,
+            client_id=client_id,
+            client_secret=client_secret,
+            quota_project_id=quota_project_id,
+            scopes=scopes,
+            default_scopes=default_scopes,
+        )
+        credential_source = credential_source or {}
+        self._environment_id = credential_source.get("environment_id") or ""
+        self._region_url = credential_source.get("region_url")
+        self._security_credentials_url = credential_source.get("url")
+        self._cred_verification_url = credential_source.get(
+            "regional_cred_verification_url"
+        )
+        self._region = None
+        self._request_signer = None
+        self._target_resource = audience
+
+        # Get the environment ID. Currently, only one version supported (v1).
+        matches = re.match(r"^(aws)([\d]+)$", self._environment_id)
+        if matches:
+            env_id, env_version = matches.groups()
+        else:
+            env_id, env_version = (None, None)
+
+        if env_id != "aws" or self._cred_verification_url is None:
+            raise ValueError("No valid AWS 'credential_source' provided")
+        elif int(env_version or "") != 1:
+            raise ValueError(
+                "aws version '{}' is not supported in the current build.".format(
+                    env_version
+                )
+            )
+
+    def retrieve_subject_token(self, request):
+        """Retrieves the subject token using the credential_source object.
+        The subject token is a serialized `AWS GetCallerIdentity signed request`_.
+
+        The logic is summarized as:
+
+        Retrieve the AWS region from the AWS_REGION or AWS_DEFAULT_REGION
+        environment variable or from the AWS metadata server availability-zone
+        if not found in the environment variable.
+
+        Check AWS credentials in environment variables. If not found, retrieve
+        from the AWS metadata server security-credentials endpoint.
+
+        When retrieving AWS credentials from the metadata server
+        security-credentials endpoint, the AWS role needs to be determined by
+        calling the security-credentials endpoint without any argument. Then the
+        credentials can be retrieved via: security-credentials/role_name
+
+        Generate the signed request to AWS STS GetCallerIdentity action.
+
+        Inject x-goog-cloud-target-resource into header and serialize the
+        signed request. This will be the subject-token to pass to GCP STS.
+
+        .. _AWS GetCallerIdentity signed request:
+            https://cloud.google.com/iam/docs/access-resources-aws#exchange-token
+
+        Args:
+            request (google.auth.transport.Request): A callable used to make
+                HTTP requests.
+        Returns:
+            str: The retrieved subject token.
+        """
+        # Initialize the request signer if not yet initialized after determining
+        # the current AWS region.
+        if self._request_signer is None:
+            self._region = self._get_region(request, self._region_url)
+            self._request_signer = RequestSigner(self._region)
+
+        # Retrieve the AWS security credentials needed to generate the signed
+        # request.
+        aws_security_credentials = self._get_security_credentials(request)
+        # Generate the signed request to AWS STS GetCallerIdentity API.
+        # Use the required regional endpoint. Otherwise, the request will fail.
+        request_options = self._request_signer.get_request_options(
+            aws_security_credentials,
+            self._cred_verification_url.replace("{region}", self._region),
+            "POST",
+        )
+        # The GCP STS endpoint expects the headers to be formatted as:
+        # [
+        #   {key: 'x-amz-date', value: '...'},
+        #   {key: 'Authorization', value: '...'},
+        #   ...
+        # ]
+        # And then serialized as:
+        # quote(json.dumps({
+        #   url: '...',
+        #   method: 'POST',
+        #   headers: [{key: 'x-amz-date', value: '...'}, ...]
+        # }))
+        request_headers = request_options.get("headers")
+        # The full, canonical resource name of the workload identity pool
+        # provider, with or without the HTTPS prefix.
+        # Including this header as part of the signature is recommended to
+        # ensure data integrity.
+        request_headers["x-goog-cloud-target-resource"] = self._target_resource
+
+        # Serialize AWS signed request.
+        # Keeping inner keys in sorted order makes testing easier for Python
+        # versions <=3.5 as the stringified JSON string would have a predictable
+        # key order.
+        aws_signed_req = {}
+        aws_signed_req["url"] = request_options.get("url")
+        aws_signed_req["method"] = request_options.get("method")
+        aws_signed_req["headers"] = []
+        # Reformat header to GCP STS expected format.
+        for key in sorted(request_headers.keys()):
+            aws_signed_req["headers"].append(
+                {"key": key, "value": request_headers[key]}
+            )
+
+        return urllib.parse.quote(
+            json.dumps(aws_signed_req, separators=(",", ":"), sort_keys=True)
+        )
+
+    def _get_region(self, request, url):
+        """Retrieves the current AWS region from either the AWS_REGION or
+        AWS_DEFAULT_REGION environment variable or from the AWS metadata server.
+
+        Args:
+            request (google.auth.transport.Request): A callable used to make
+                HTTP requests.
+            url (str): The AWS metadata server region URL.
+
+        Returns:
+            str: The current AWS region.
+
+        Raises:
+            google.auth.exceptions.RefreshError: If an error occurs while
+                retrieving the AWS region.
+        """
+        # The AWS metadata server is not available in some AWS environments
+        # such as AWS lambda. Instead, it is available via environment
+        # variable.
+        env_aws_region = os.environ.get(environment_vars.AWS_REGION)
+        if env_aws_region is not None:
+            return env_aws_region
+
+        env_aws_region = os.environ.get(environment_vars.AWS_DEFAULT_REGION)
+        if env_aws_region is not None:
+            return env_aws_region
+
+        if not self._region_url:
+            raise exceptions.RefreshError("Unable to determine AWS region")
+        response = request(url=self._region_url, method="GET")
+
+        # Support both string and bytes type response.data.
+        response_body = (
+            response.data.decode("utf-8")
+            if hasattr(response.data, "decode")
+            else response.data
+        )
+
+        if response.status != 200:
+            raise exceptions.RefreshError(
+                "Unable to retrieve AWS region", response_body
+            )
+
+        # This endpoint will return the region in format: us-east-2b.
+        # Only the us-east-2 part should be used.
+        return response_body[:-1]
+
+    def _get_security_credentials(self, request):
+        """Retrieves the AWS security credentials required for signing AWS
+        requests from either the AWS security credentials environment variables
+        or from the AWS metadata server.
+
+        Args:
+            request (google.auth.transport.Request): A callable used to make
+                HTTP requests.
+
+        Returns:
+            Mapping[str, str]: The AWS security credentials dictionary object.
+
+        Raises:
+            google.auth.exceptions.RefreshError: If an error occurs while
+                retrieving the AWS security credentials.
+        """
+
+        # Check environment variables for permanent credentials first.
+        # https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html
+        env_aws_access_key_id = os.environ.get(environment_vars.AWS_ACCESS_KEY_ID)
+        env_aws_secret_access_key = os.environ.get(
+            environment_vars.AWS_SECRET_ACCESS_KEY
+        )
+        # This is normally not available for permanent credentials.
+        env_aws_session_token = os.environ.get(environment_vars.AWS_SESSION_TOKEN)
+        if env_aws_access_key_id and env_aws_secret_access_key:
+            return {
+                "access_key_id": env_aws_access_key_id,
+                "secret_access_key": env_aws_secret_access_key,
+                "security_token": env_aws_session_token,
+            }
+
+        # Get role name.
+        role_name = self._get_metadata_role_name(request)
+
+        # Get security credentials.
+        credentials = self._get_metadata_security_credentials(request, role_name)
+
+        return {
+            "access_key_id": credentials.get("AccessKeyId"),
+            "secret_access_key": credentials.get("SecretAccessKey"),
+            "security_token": credentials.get("Token"),
+        }
+
+    def _get_metadata_security_credentials(self, request, role_name):
+        """Retrieves the AWS security credentials required for signing AWS
+        requests from the AWS metadata server.
+
+        Args:
+            request (google.auth.transport.Request): A callable used to make
+                HTTP requests.
+            role_name (str): The AWS role name required by the AWS metadata
+                server security_credentials endpoint in order to return the
+                credentials.
+
+        Returns:
+            Mapping[str, str]: The AWS metadata server security credentials
+                response.
+
+        Raises:
+            google.auth.exceptions.RefreshError: If an error occurs while
+                retrieving the AWS security credentials.
+        """
+        headers = {"Content-Type": "application/json"}
+        response = request(
+            url="{}/{}".format(self._security_credentials_url, role_name),
+            method="GET",
+            headers=headers,
+        )
+
+        # support both string and bytes type response.data
+        response_body = (
+            response.data.decode("utf-8")
+            if hasattr(response.data, "decode")
+            else response.data
+        )
+
+        if response.status != http_client.OK:
+            raise exceptions.RefreshError(
+                "Unable to retrieve AWS security credentials", response_body
+            )
+
+        credentials_response = json.loads(response_body)
+
+        return credentials_response
+
+    def _get_metadata_role_name(self, request):
+        """Retrieves the AWS role currently attached to the current AWS
+        workload by querying the AWS metadata server. This is needed for the
+        AWS metadata server security credentials endpoint in order to retrieve
+        the AWS security credentials needed to sign requests to AWS APIs.
+
+        Args:
+            request (google.auth.transport.Request): A callable used to make
+                HTTP requests.
+
+        Returns:
+            str: The AWS role name.
+
+        Raises:
+            google.auth.exceptions.RefreshError: If an error occurs while
+                retrieving the AWS role name.
+        """
+        if self._security_credentials_url is None:
+            raise exceptions.RefreshError(
+                "Unable to determine the AWS metadata server security credentials endpoint"
+            )
+        response = request(url=self._security_credentials_url, method="GET")
+
+        # support both string and bytes type response.data
+        response_body = (
+            response.data.decode("utf-8")
+            if hasattr(response.data, "decode")
+            else response.data
+        )
+
+        if response.status != http_client.OK:
+            raise exceptions.RefreshError(
+                "Unable to retrieve AWS role name", response_body
+            )
+
+        return response_body
+
+    @classmethod
+    def from_info(cls, info, **kwargs):
+        """Creates an AWS Credentials instance from parsed external account info.
+
+        Args:
+            info (Mapping[str, str]): The AWS external account info in Google
+                format.
+            kwargs: Additional arguments to pass to the constructor.
+
+        Returns:
+            google.auth.aws.Credentials: The constructed credentials.
+
+        Raises:
+            ValueError: For invalid parameters.
+        """
+        return cls(
+            audience=info.get("audience"),
+            subject_token_type=info.get("subject_token_type"),
+            token_url=info.get("token_url"),
+            service_account_impersonation_url=info.get(
+                "service_account_impersonation_url"
+            ),
+            client_id=info.get("client_id"),
+            client_secret=info.get("client_secret"),
+            credential_source=info.get("credential_source"),
+            quota_project_id=info.get("quota_project_id"),
+            **kwargs
+        )
+
+    @classmethod
+    def from_file(cls, filename, **kwargs):
+        """Creates an AWS Credentials instance from an external account json file.
+
+        Args:
+            filename (str): The path to the AWS external account json file.
+            kwargs: Additional arguments to pass to the constructor.
+
+        Returns:
+            google.auth.aws.Credentials: The constructed credentials.
+        """
+        with io.open(filename, "r", encoding="utf-8") as json_file:
+            data = json.load(json_file)
+            return cls.from_info(data, **kwargs)
diff --git a/google/auth/compute_engine/__init__.py b/google/auth/compute_engine/__init__.py
new file mode 100644
index 0000000..5c84234
--- /dev/null
+++ b/google/auth/compute_engine/__init__.py
@@ -0,0 +1,21 @@
+# Copyright 2016 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.
+
+"""Google Compute Engine authentication."""
+
+from google.auth.compute_engine.credentials import Credentials
+from google.auth.compute_engine.credentials import IDTokenCredentials
+
+
+__all__ = ["Credentials", "IDTokenCredentials"]
diff --git a/google/auth/compute_engine/_metadata.py b/google/auth/compute_engine/_metadata.py
new file mode 100644
index 0000000..9db7bea
--- /dev/null
+++ b/google/auth/compute_engine/_metadata.py
@@ -0,0 +1,267 @@
+# Copyright 2016 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.
+
+"""Provides helper methods for talking to the Compute Engine metadata server.
+
+See https://cloud.google.com/compute/docs/metadata for more details.
+"""
+
+import datetime
+import json
+import logging
+import os
+
+import six
+from six.moves import http_client
+from six.moves.urllib import parse as urlparse
+
+from google.auth import _helpers
+from google.auth import environment_vars
+from google.auth import exceptions
+
+_LOGGER = logging.getLogger(__name__)
+
+# Environment variable GCE_METADATA_HOST is originally named
+# GCE_METADATA_ROOT. For compatiblity reasons, here it checks
+# the new variable first; if not set, the system falls back
+# to the old variable.
+_GCE_METADATA_HOST = os.getenv(environment_vars.GCE_METADATA_HOST, None)
+if not _GCE_METADATA_HOST:
+    _GCE_METADATA_HOST = os.getenv(
+        environment_vars.GCE_METADATA_ROOT, "metadata.google.internal"
+    )
+_METADATA_ROOT = "http://{}/computeMetadata/v1/".format(_GCE_METADATA_HOST)
+
+# This is used to ping the metadata server, it avoids the cost of a DNS
+# lookup.
+_METADATA_IP_ROOT = "http://{}".format(
+    os.getenv(environment_vars.GCE_METADATA_IP, "169.254.169.254")
+)
+_METADATA_FLAVOR_HEADER = "metadata-flavor"
+_METADATA_FLAVOR_VALUE = "Google"
+_METADATA_HEADERS = {_METADATA_FLAVOR_HEADER: _METADATA_FLAVOR_VALUE}
+
+# Timeout in seconds to wait for the GCE metadata server when detecting the
+# GCE environment.
+try:
+    _METADATA_DEFAULT_TIMEOUT = int(os.getenv("GCE_METADATA_TIMEOUT", 3))
+except ValueError:  # pragma: NO COVER
+    _METADATA_DEFAULT_TIMEOUT = 3
+
+
+def ping(request, timeout=_METADATA_DEFAULT_TIMEOUT, retry_count=3):
+    """Checks to see if the metadata server is available.
+
+    Args:
+        request (google.auth.transport.Request): A callable used to make
+            HTTP requests.
+        timeout (int): How long to wait for the metadata server to respond.
+        retry_count (int): How many times to attempt connecting to metadata
+            server using above timeout.
+
+    Returns:
+        bool: True if the metadata server is reachable, False otherwise.
+    """
+    # NOTE: The explicit ``timeout`` is a workaround. The underlying
+    #       issue is that resolving an unknown host on some networks will take
+    #       20-30 seconds; making this timeout short fixes the issue, but
+    #       could lead to false negatives in the event that we are on GCE, but
+    #       the metadata resolution was particularly slow. The latter case is
+    #       "unlikely".
+    retries = 0
+    while retries < retry_count:
+        try:
+            response = request(
+                url=_METADATA_IP_ROOT,
+                method="GET",
+                headers=_METADATA_HEADERS,
+                timeout=timeout,
+            )
+
+            metadata_flavor = response.headers.get(_METADATA_FLAVOR_HEADER)
+            return (
+                response.status == http_client.OK
+                and metadata_flavor == _METADATA_FLAVOR_VALUE
+            )
+
+        except exceptions.TransportError as e:
+            _LOGGER.warning(
+                "Compute Engine Metadata server unavailable on "
+                "attempt %s of %s. Reason: %s",
+                retries + 1,
+                retry_count,
+                e,
+            )
+            retries += 1
+
+    return False
+
+
+def get(
+    request, path, root=_METADATA_ROOT, params=None, recursive=False, retry_count=5
+):
+    """Fetch a resource from the metadata server.
+
+    Args:
+        request (google.auth.transport.Request): A callable used to make
+            HTTP requests.
+        path (str): The resource to retrieve. For example,
+            ``'instance/service-accounts/default'``.
+        root (str): The full path to the metadata server root.
+        params (Optional[Mapping[str, str]]): A mapping of query parameter
+            keys to values.
+        recursive (bool): Whether to do a recursive query of metadata. See
+            https://cloud.google.com/compute/docs/metadata#aggcontents for more
+            details.
+        retry_count (int): How many times to attempt connecting to metadata
+            server using above timeout.
+
+    Returns:
+        Union[Mapping, str]: If the metadata server returns JSON, a mapping of
+            the decoded JSON is return. Otherwise, the response content is
+            returned as a string.
+
+    Raises:
+        google.auth.exceptions.TransportError: if an error occurred while
+            retrieving metadata.
+    """
+    base_url = urlparse.urljoin(root, path)
+    query_params = {} if params is None else params
+
+    if recursive:
+        query_params["recursive"] = "true"
+
+    url = _helpers.update_query(base_url, query_params)
+
+    retries = 0
+    while retries < retry_count:
+        try:
+            response = request(url=url, method="GET", headers=_METADATA_HEADERS)
+            break
+
+        except exceptions.TransportError as e:
+            _LOGGER.warning(
+                "Compute Engine Metadata server unavailable on "
+                "attempt %s of %s. Reason: %s",
+                retries + 1,
+                retry_count,
+                e,
+            )
+            retries += 1
+    else:
+        raise exceptions.TransportError(
+            "Failed to retrieve {} from the Google Compute Engine"
+            "metadata service. Compute Engine Metadata server unavailable".format(url)
+        )
+
+    if response.status == http_client.OK:
+        content = _helpers.from_bytes(response.data)
+        if response.headers["content-type"] == "application/json":
+            try:
+                return json.loads(content)
+            except ValueError as caught_exc:
+                new_exc = exceptions.TransportError(
+                    "Received invalid JSON from the Google Compute Engine"
+                    "metadata service: {:.20}".format(content)
+                )
+                six.raise_from(new_exc, caught_exc)
+        else:
+            return content
+    else:
+        raise exceptions.TransportError(
+            "Failed to retrieve {} from the Google Compute Engine"
+            "metadata service. Status: {} Response:\n{}".format(
+                url, response.status, response.data
+            ),
+            response,
+        )
+
+
+def get_project_id(request):
+    """Get the Google Cloud Project ID from the metadata server.
+
+    Args:
+        request (google.auth.transport.Request): A callable used to make
+            HTTP requests.
+
+    Returns:
+        str: The project ID
+
+    Raises:
+        google.auth.exceptions.TransportError: if an error occurred while
+            retrieving metadata.
+    """
+    return get(request, "project/project-id")
+
+
+def get_service_account_info(request, service_account="default"):
+    """Get information about a service account from the metadata server.
+
+    Args:
+        request (google.auth.transport.Request): A callable used to make
+            HTTP requests.
+        service_account (str): The string 'default' or a service account email
+            address. The determines which service account for which to acquire
+            information.
+
+    Returns:
+        Mapping: The service account's information, for example::
+
+            {
+                'email': '...',
+                'scopes': ['scope', ...],
+                'aliases': ['default', '...']
+            }
+
+    Raises:
+        google.auth.exceptions.TransportError: if an error occurred while
+            retrieving metadata.
+    """
+    path = "instance/service-accounts/{0}/".format(service_account)
+    # See https://cloud.google.com/compute/docs/metadata#aggcontents
+    # for more on the use of 'recursive'.
+    return get(request, path, params={"recursive": "true"})
+
+
+def get_service_account_token(request, service_account="default", scopes=None):
+    """Get the OAuth 2.0 access token for a service account.
+
+    Args:
+        request (google.auth.transport.Request): A callable used to make
+            HTTP requests.
+        service_account (str): The string 'default' or a service account email
+            address. The determines which service account for which to acquire
+            an access token.
+        scopes (Optional[Union[str, List[str]]]): Optional string or list of
+            strings with auth scopes.
+    Returns:
+        Union[str, datetime]: The access token and its expiration.
+
+    Raises:
+        google.auth.exceptions.TransportError: if an error occurred while
+            retrieving metadata.
+    """
+    if scopes:
+        if not isinstance(scopes, str):
+            scopes = ",".join(scopes)
+        params = {"scopes": scopes}
+    else:
+        params = None
+
+    path = "instance/service-accounts/{0}/token".format(service_account)
+    token_json = get(request, path, params=params)
+    token_expiry = _helpers.utcnow() + datetime.timedelta(
+        seconds=token_json["expires_in"]
+    )
+    return token_json["access_token"], token_expiry
diff --git a/google/auth/compute_engine/credentials.py b/google/auth/compute_engine/credentials.py
new file mode 100644
index 0000000..b39ac50
--- /dev/null
+++ b/google/auth/compute_engine/credentials.py
@@ -0,0 +1,413 @@
+# Copyright 2016 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.
+
+"""Google Compute Engine credentials.
+
+This module provides authentication for an application running on Google
+Compute Engine using the Compute Engine metadata server.
+
+"""
+
+import datetime
+
+import six
+
+from google.auth import _helpers
+from google.auth import credentials
+from google.auth import exceptions
+from google.auth import iam
+from google.auth import jwt
+from google.auth.compute_engine import _metadata
+from google.oauth2 import _client
+
+
+class Credentials(credentials.Scoped, credentials.CredentialsWithQuotaProject):
+    """Compute Engine Credentials.
+
+    These credentials use the Google Compute Engine metadata server to obtain
+    OAuth 2.0 access tokens associated with the instance's service account,
+    and are also used for Cloud Run, Flex and App Engine (except for the Python
+    2.7 runtime, which is supported only on older versions of this library).
+
+    For more information about Compute Engine authentication, including how
+    to configure scopes, see the `Compute Engine authentication
+    documentation`_.
+
+    .. note:: On Compute Engine the metadata server ignores requested scopes.
+        On Cloud Run, Flex and App Engine the server honours requested scopes.
+
+    .. _Compute Engine authentication documentation:
+        https://cloud.google.com/compute/docs/authentication#using
+    """
+
+    def __init__(
+        self,
+        service_account_email="default",
+        quota_project_id=None,
+        scopes=None,
+        default_scopes=None,
+    ):
+        """
+        Args:
+            service_account_email (str): The service account email to use, or
+                'default'. A Compute Engine instance may have multiple service
+                accounts.
+            quota_project_id (Optional[str]): The project ID used for quota and
+                billing.
+            scopes (Optional[Sequence[str]]): The list of scopes for the credentials.
+            default_scopes (Optional[Sequence[str]]): Default scopes passed by a
+                Google client library. Use 'scopes' for user-defined scopes.
+        """
+        super(Credentials, self).__init__()
+        self._service_account_email = service_account_email
+        self._quota_project_id = quota_project_id
+        self._scopes = scopes
+        self._default_scopes = default_scopes
+
+    def _retrieve_info(self, request):
+        """Retrieve information about the service account.
+
+        Updates the scopes and retrieves the full service account email.
+
+        Args:
+            request (google.auth.transport.Request): The object used to make
+                HTTP requests.
+        """
+        info = _metadata.get_service_account_info(
+            request, service_account=self._service_account_email
+        )
+
+        self._service_account_email = info["email"]
+
+        # Don't override scopes requested by the user.
+        if self._scopes is None:
+            self._scopes = info["scopes"]
+
+    def refresh(self, request):
+        """Refresh the access token and scopes.
+
+        Args:
+            request (google.auth.transport.Request): The object used to make
+                HTTP requests.
+
+        Raises:
+            google.auth.exceptions.RefreshError: If the Compute Engine metadata
+                service can't be reached if if the instance has not
+                credentials.
+        """
+        scopes = self._scopes if self._scopes is not None else self._default_scopes
+        try:
+            self._retrieve_info(request)
+            self.token, self.expiry = _metadata.get_service_account_token(
+                request, service_account=self._service_account_email, scopes=scopes
+            )
+        except exceptions.TransportError as caught_exc:
+            new_exc = exceptions.RefreshError(caught_exc)
+            six.raise_from(new_exc, caught_exc)
+
+    @property
+    def service_account_email(self):
+        """The service account email.
+
+        .. note:: This is not guaranteed to be set until :meth:`refresh` has been
+            called.
+        """
+        return self._service_account_email
+
+    @property
+    def requires_scopes(self):
+        return not self._scopes
+
+    @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
+    def with_quota_project(self, quota_project_id):
+        return self.__class__(
+            service_account_email=self._service_account_email,
+            quota_project_id=quota_project_id,
+            scopes=self._scopes,
+        )
+
+    @_helpers.copy_docstring(credentials.Scoped)
+    def with_scopes(self, scopes, default_scopes=None):
+        # Compute Engine credentials can not be scoped (the metadata service
+        # ignores the scopes parameter). App Engine, Cloud Run and Flex support
+        # requesting scopes.
+        return self.__class__(
+            scopes=scopes,
+            default_scopes=default_scopes,
+            service_account_email=self._service_account_email,
+            quota_project_id=self._quota_project_id,
+        )
+
+
+_DEFAULT_TOKEN_LIFETIME_SECS = 3600  # 1 hour in seconds
+_DEFAULT_TOKEN_URI = "https://www.googleapis.com/oauth2/v4/token"
+
+
+class IDTokenCredentials(credentials.CredentialsWithQuotaProject, credentials.Signing):
+    """Open ID Connect ID Token-based service account credentials.
+
+    These credentials relies on the default service account of a GCE instance.
+
+    ID token can be requested from `GCE metadata server identity endpoint`_, IAM
+    token endpoint or other token endpoints you specify. If metadata server
+    identity endpoint is not used, the GCE instance must have been started with
+    a service account that has access to the IAM Cloud API.
+
+    .. _GCE metadata server identity endpoint:
+        https://cloud.google.com/compute/docs/instances/verifying-instance-identity
+    """
+
+    def __init__(
+        self,
+        request,
+        target_audience,
+        token_uri=None,
+        additional_claims=None,
+        service_account_email=None,
+        signer=None,
+        use_metadata_identity_endpoint=False,
+        quota_project_id=None,
+    ):
+        """
+        Args:
+            request (google.auth.transport.Request): The object used to make
+                HTTP requests.
+            target_audience (str): The intended audience for these credentials,
+                used when requesting the ID Token. The ID Token's ``aud`` claim
+                will be set to this string.
+            token_uri (str): The OAuth 2.0 Token URI.
+            additional_claims (Mapping[str, str]): Any additional claims for
+                the JWT assertion used in the authorization grant.
+            service_account_email (str): Optional explicit service account to
+                use to sign JWT tokens.
+                By default, this is the default GCE service account.
+            signer (google.auth.crypt.Signer): The signer used to sign JWTs.
+                In case the signer is specified, the request argument will be
+                ignored.
+            use_metadata_identity_endpoint (bool): Whether to use GCE metadata
+                identity endpoint. For backward compatibility the default value
+                is False. If set to True, ``token_uri``, ``additional_claims``,
+                ``service_account_email``, ``signer`` argument should not be set;
+                otherwise ValueError will be raised.
+            quota_project_id (Optional[str]): The project ID used for quota and
+                billing.
+
+        Raises:
+            ValueError:
+                If ``use_metadata_identity_endpoint`` is set to True, and one of
+                ``token_uri``, ``additional_claims``, ``service_account_email``,
+                 ``signer`` arguments is set.
+        """
+        super(IDTokenCredentials, self).__init__()
+
+        self._quota_project_id = quota_project_id
+        self._use_metadata_identity_endpoint = use_metadata_identity_endpoint
+        self._target_audience = target_audience
+
+        if use_metadata_identity_endpoint:
+            if token_uri or additional_claims or service_account_email or signer:
+                raise ValueError(
+                    "If use_metadata_identity_endpoint is set, token_uri, "
+                    "additional_claims, service_account_email, signer arguments"
+                    " must not be set"
+                )
+            self._token_uri = None
+            self._additional_claims = None
+            self._signer = None
+
+        if service_account_email is None:
+            sa_info = _metadata.get_service_account_info(request)
+            self._service_account_email = sa_info["email"]
+        else:
+            self._service_account_email = service_account_email
+
+        if not use_metadata_identity_endpoint:
+            if signer is None:
+                signer = iam.Signer(
+                    request=request,
+                    credentials=Credentials(),
+                    service_account_email=self._service_account_email,
+                )
+            self._signer = signer
+            self._token_uri = token_uri or _DEFAULT_TOKEN_URI
+
+            if additional_claims is not None:
+                self._additional_claims = additional_claims
+            else:
+                self._additional_claims = {}
+
+    def with_target_audience(self, target_audience):
+        """Create a copy of these credentials with the specified target
+        audience.
+        Args:
+            target_audience (str): The intended audience for these credentials,
+            used when requesting the ID Token.
+        Returns:
+            google.auth.service_account.IDTokenCredentials: A new credentials
+                instance.
+        """
+        # since the signer is already instantiated,
+        # the request is not needed
+        if self._use_metadata_identity_endpoint:
+            return self.__class__(
+                None,
+                target_audience=target_audience,
+                use_metadata_identity_endpoint=True,
+                quota_project_id=self._quota_project_id,
+            )
+        else:
+            return self.__class__(
+                None,
+                service_account_email=self._service_account_email,
+                token_uri=self._token_uri,
+                target_audience=target_audience,
+                additional_claims=self._additional_claims.copy(),
+                signer=self.signer,
+                use_metadata_identity_endpoint=False,
+                quota_project_id=self._quota_project_id,
+            )
+
+    @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
+    def with_quota_project(self, quota_project_id):
+
+        # since the signer is already instantiated,
+        # the request is not needed
+        if self._use_metadata_identity_endpoint:
+            return self.__class__(
+                None,
+                target_audience=self._target_audience,
+                use_metadata_identity_endpoint=True,
+                quota_project_id=quota_project_id,
+            )
+        else:
+            return self.__class__(
+                None,
+                service_account_email=self._service_account_email,
+                token_uri=self._token_uri,
+                target_audience=self._target_audience,
+                additional_claims=self._additional_claims.copy(),
+                signer=self.signer,
+                use_metadata_identity_endpoint=False,
+                quota_project_id=quota_project_id,
+            )
+
+    def _make_authorization_grant_assertion(self):
+        """Create the OAuth 2.0 assertion.
+        This assertion is used during the OAuth 2.0 grant to acquire an
+        ID token.
+        Returns:
+            bytes: The authorization grant assertion.
+        """
+        now = _helpers.utcnow()
+        lifetime = datetime.timedelta(seconds=_DEFAULT_TOKEN_LIFETIME_SECS)
+        expiry = now + lifetime
+
+        payload = {
+            "iat": _helpers.datetime_to_secs(now),
+            "exp": _helpers.datetime_to_secs(expiry),
+            # The issuer must be the service account email.
+            "iss": self.service_account_email,
+            # The audience must be the auth token endpoint's URI
+            "aud": self._token_uri,
+            # The target audience specifies which service the ID token is
+            # intended for.
+            "target_audience": self._target_audience,
+        }
+
+        payload.update(self._additional_claims)
+
+        token = jwt.encode(self._signer, payload)
+
+        return token
+
+    def _call_metadata_identity_endpoint(self, request):
+        """Request ID token from metadata identity endpoint.
+
+        Args:
+            request (google.auth.transport.Request): The object used to make
+                HTTP requests.
+
+        Returns:
+            Tuple[str, datetime.datetime]: The ID token and the expiry of the ID token.
+
+        Raises:
+            google.auth.exceptions.RefreshError: If the Compute Engine metadata
+                service can't be reached or if the instance has no credentials.
+            ValueError: If extracting expiry from the obtained ID token fails.
+        """
+        try:
+            path = "instance/service-accounts/default/identity"
+            params = {"audience": self._target_audience, "format": "full"}
+            id_token = _metadata.get(request, path, params=params)
+        except exceptions.TransportError as caught_exc:
+            new_exc = exceptions.RefreshError(caught_exc)
+            six.raise_from(new_exc, caught_exc)
+
+        _, payload, _, _ = jwt._unverified_decode(id_token)
+        return id_token, datetime.datetime.fromtimestamp(payload["exp"])
+
+    def refresh(self, request):
+        """Refreshes the ID token.
+
+        Args:
+            request (google.auth.transport.Request): The object used to make
+                HTTP requests.
+
+        Raises:
+            google.auth.exceptions.RefreshError: If the credentials could
+                not be refreshed.
+            ValueError: If extracting expiry from the obtained ID token fails.
+        """
+        if self._use_metadata_identity_endpoint:
+            self.token, self.expiry = self._call_metadata_identity_endpoint(request)
+        else:
+            assertion = self._make_authorization_grant_assertion()
+            access_token, expiry, _ = _client.id_token_jwt_grant(
+                request, self._token_uri, assertion
+            )
+            self.token = access_token
+            self.expiry = expiry
+
+    @property
+    @_helpers.copy_docstring(credentials.Signing)
+    def signer(self):
+        return self._signer
+
+    def sign_bytes(self, message):
+        """Signs the given message.
+
+        Args:
+            message (bytes): The message to sign.
+
+        Returns:
+            bytes: The message's cryptographic signature.
+
+        Raises:
+            ValueError:
+                Signer is not available if metadata identity endpoint is used.
+        """
+        if self._use_metadata_identity_endpoint:
+            raise ValueError(
+                "Signer is not available if metadata identity endpoint is used"
+            )
+        return self._signer.sign(message)
+
+    @property
+    def service_account_email(self):
+        """The service account email."""
+        return self._service_account_email
+
+    @property
+    def signer_email(self):
+        return self._service_account_email
diff --git a/google/auth/credentials.py b/google/auth/credentials.py
new file mode 100644
index 0000000..ec21a27
--- /dev/null
+++ b/google/auth/credentials.py
@@ -0,0 +1,362 @@
+# Copyright 2016 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.
+
+
+"""Interfaces for credentials."""
+
+import abc
+
+import six
+
+from google.auth import _helpers
+
+
[email protected]_metaclass(abc.ABCMeta)
+class Credentials(object):
+    """Base class for all credentials.
+
+    All credentials have a :attr:`token` that is used for authentication and
+    may also optionally set an :attr:`expiry` to indicate when the token will
+    no longer be valid.
+
+    Most credentials will be :attr:`invalid` until :meth:`refresh` is called.
+    Credentials can do this automatically before the first HTTP request in
+    :meth:`before_request`.
+
+    Although the token and expiration will change as the credentials are
+    :meth:`refreshed <refresh>` and used, credentials should be considered
+    immutable. Various credentials will accept configuration such as private
+    keys, scopes, and other options. These options are not changeable after
+    construction. Some classes will provide mechanisms to copy the credentials
+    with modifications such as :meth:`ScopedCredentials.with_scopes`.
+    """
+
+    def __init__(self):
+        self.token = None
+        """str: The bearer token that can be used in HTTP headers to make
+        authenticated requests."""
+        self.expiry = None
+        """Optional[datetime]: When the token expires and is no longer valid.
+        If this is None, the token is assumed to never expire."""
+        self._quota_project_id = None
+        """Optional[str]: Project to use for quota and billing purposes."""
+
+    @property
+    def expired(self):
+        """Checks if the credentials are expired.
+
+        Note that credentials can be invalid but not expired because
+        Credentials with :attr:`expiry` set to None is considered to never
+        expire.
+        """
+        if not self.expiry:
+            return False
+
+        # Remove 10 seconds from expiry to err on the side of reporting
+        # expiration early so that we avoid the 401-refresh-retry loop.
+        skewed_expiry = self.expiry - _helpers.REFRESH_THRESHOLD
+        return _helpers.utcnow() >= skewed_expiry
+
+    @property
+    def valid(self):
+        """Checks the validity of the credentials.
+
+        This is True if the credentials have a :attr:`token` and the token
+        is not :attr:`expired`.
+        """
+        return self.token is not None and not self.expired
+
+    @property
+    def quota_project_id(self):
+        """Project to use for quota and billing purposes."""
+        return self._quota_project_id
+
+    @abc.abstractmethod
+    def refresh(self, request):
+        """Refreshes the access token.
+
+        Args:
+            request (google.auth.transport.Request): The object used to make
+                HTTP requests.
+
+        Raises:
+            google.auth.exceptions.RefreshError: If the credentials could
+                not be refreshed.
+        """
+        # pylint: disable=missing-raises-doc
+        # (pylint doesn't recognize that this is abstract)
+        raise NotImplementedError("Refresh must be implemented")
+
+    def apply(self, headers, token=None):
+        """Apply the token to the authentication header.
+
+        Args:
+            headers (Mapping): The HTTP request headers.
+            token (Optional[str]): If specified, overrides the current access
+                token.
+        """
+        headers["authorization"] = "Bearer {}".format(
+            _helpers.from_bytes(token or self.token)
+        )
+        if self.quota_project_id:
+            headers["x-goog-user-project"] = self.quota_project_id
+
+    def before_request(self, request, method, url, headers):
+        """Performs credential-specific before request logic.
+
+        Refreshes the credentials if necessary, then calls :meth:`apply` to
+        apply the token to the authentication header.
+
+        Args:
+            request (google.auth.transport.Request): The object used to make
+                HTTP requests.
+            method (str): The request's HTTP method or the RPC method being
+                invoked.
+            url (str): The request's URI or the RPC service's URI.
+            headers (Mapping): The request's headers.
+        """
+        # pylint: disable=unused-argument
+        # (Subclasses may use these arguments to ascertain information about
+        # the http request.)
+        if not self.valid:
+            self.refresh(request)
+        self.apply(headers)
+
+
+class CredentialsWithQuotaProject(Credentials):
+    """Abstract base for credentials supporting ``with_quota_project`` factory"""
+
+    def with_quota_project(self, quota_project_id):
+        """Returns a copy of these credentials with a modified quota project.
+
+        Args:
+            quota_project_id (str): The project to use for quota and
+                billing purposes
+
+        Returns:
+            google.oauth2.credentials.Credentials: A new credentials instance.
+        """
+        raise NotImplementedError("This credential does not support quota project.")
+
+
+class AnonymousCredentials(Credentials):
+    """Credentials that do not provide any authentication information.
+
+    These are useful in the case of services that support anonymous access or
+    local service emulators that do not use credentials.
+    """
+
+    @property
+    def expired(self):
+        """Returns `False`, anonymous credentials never expire."""
+        return False
+
+    @property
+    def valid(self):
+        """Returns `True`, anonymous credentials are always valid."""
+        return True
+
+    def refresh(self, request):
+        """Raises :class:`ValueError``, anonymous credentials cannot be
+        refreshed."""
+        raise ValueError("Anonymous credentials cannot be refreshed.")
+
+    def apply(self, headers, token=None):
+        """Anonymous credentials do nothing to the request.
+
+        The optional ``token`` argument is not supported.
+
+        Raises:
+            ValueError: If a token was specified.
+        """
+        if token is not None:
+            raise ValueError("Anonymous credentials don't support tokens.")
+
+    def before_request(self, request, method, url, headers):
+        """Anonymous credentials do nothing to the request."""
+
+
[email protected]_metaclass(abc.ABCMeta)
+class ReadOnlyScoped(object):
+    """Interface for credentials whose scopes can be queried.
+
+    OAuth 2.0-based credentials allow limiting access using scopes as described
+    in `RFC6749 Section 3.3`_.
+    If a credential class implements this interface then the credentials either
+    use scopes in their implementation.
+
+    Some credentials require scopes in order to obtain a token. You can check
+    if scoping is necessary with :attr:`requires_scopes`::
+
+        if credentials.requires_scopes:
+            # Scoping is required.
+            credentials = credentials.with_scopes(scopes=['one', 'two'])
+
+    Credentials that require scopes must either be constructed with scopes::
+
+        credentials = SomeScopedCredentials(scopes=['one', 'two'])
+
+    Or must copy an existing instance using :meth:`with_scopes`::
+
+        scoped_credentials = credentials.with_scopes(scopes=['one', 'two'])
+
+    Some credentials have scopes but do not allow or require scopes to be set,
+    these credentials can be used as-is.
+
+    .. _RFC6749 Section 3.3: https://tools.ietf.org/html/rfc6749#section-3.3
+    """
+
+    def __init__(self):
+        super(ReadOnlyScoped, self).__init__()
+        self._scopes = None
+        self._default_scopes = None
+
+    @property
+    def scopes(self):
+        """Sequence[str]: the credentials' current set of scopes."""
+        return self._scopes
+
+    @property
+    def default_scopes(self):
+        """Sequence[str]: the credentials' current set of default scopes."""
+        return self._default_scopes
+
+    @abc.abstractproperty
+    def requires_scopes(self):
+        """True if these credentials require scopes to obtain an access token.
+        """
+        return False
+
+    def has_scopes(self, scopes):
+        """Checks if the credentials have the given scopes.
+
+        .. warning: This method is not guaranteed to be accurate if the
+            credentials are :attr:`~Credentials.invalid`.
+
+        Args:
+            scopes (Sequence[str]): The list of scopes to check.
+
+        Returns:
+            bool: True if the credentials have the given scopes.
+        """
+        credential_scopes = (
+            self._scopes if self._scopes is not None else self._default_scopes
+        )
+        return set(scopes).issubset(set(credential_scopes or []))
+
+
+class Scoped(ReadOnlyScoped):
+    """Interface for credentials whose scopes can be replaced while copying.
+
+    OAuth 2.0-based credentials allow limiting access using scopes as described
+    in `RFC6749 Section 3.3`_.
+    If a credential class implements this interface then the credentials either
+    use scopes in their implementation.
+
+    Some credentials require scopes in order to obtain a token. You can check
+    if scoping is necessary with :attr:`requires_scopes`::
+
+        if credentials.requires_scopes:
+            # Scoping is required.
+            credentials = credentials.create_scoped(['one', 'two'])
+
+    Credentials that require scopes must either be constructed with scopes::
+
+        credentials = SomeScopedCredentials(scopes=['one', 'two'])
+
+    Or must copy an existing instance using :meth:`with_scopes`::
+
+        scoped_credentials = credentials.with_scopes(scopes=['one', 'two'])
+
+    Some credentials have scopes but do not allow or require scopes to be set,
+    these credentials can be used as-is.
+
+    .. _RFC6749 Section 3.3: https://tools.ietf.org/html/rfc6749#section-3.3
+    """
+
+    @abc.abstractmethod
+    def with_scopes(self, scopes, default_scopes=None):
+        """Create a copy of these credentials with the specified scopes.
+
+        Args:
+            scopes (Sequence[str]): The list of scopes to attach to the
+                current credentials.
+
+        Raises:
+            NotImplementedError: If the credentials' scopes can not be changed.
+                This can be avoided by checking :attr:`requires_scopes` before
+                calling this method.
+        """
+        raise NotImplementedError("This class does not require scoping.")
+
+
+def with_scopes_if_required(credentials, scopes, default_scopes=None):
+    """Creates a copy of the credentials with scopes if scoping is required.
+
+    This helper function is useful when you do not know (or care to know) the
+    specific type of credentials you are using (such as when you use
+    :func:`google.auth.default`). This function will call
+    :meth:`Scoped.with_scopes` if the credentials are scoped credentials and if
+    the credentials require scoping. Otherwise, it will return the credentials
+    as-is.
+
+    Args:
+        credentials (google.auth.credentials.Credentials): The credentials to
+            scope if necessary.
+        scopes (Sequence[str]): The list of scopes to use.
+        default_scopes (Sequence[str]): Default scopes passed by a
+            Google client library. Use 'scopes' for user-defined scopes.
+
+    Returns:
+        google.auth.credentials.Credentials: Either a new set of scoped
+            credentials, or the passed in credentials instance if no scoping
+            was required.
+    """
+    if isinstance(credentials, Scoped) and credentials.requires_scopes:
+        return credentials.with_scopes(scopes, default_scopes=default_scopes)
+    else:
+        return credentials
+
+
[email protected]_metaclass(abc.ABCMeta)
+class Signing(object):
+    """Interface for credentials that can cryptographically sign messages."""
+
+    @abc.abstractmethod
+    def sign_bytes(self, message):
+        """Signs the given message.
+
+        Args:
+            message (bytes): The message to sign.
+
+        Returns:
+            bytes: The message's cryptographic signature.
+        """
+        # pylint: disable=missing-raises-doc,redundant-returns-doc
+        # (pylint doesn't recognize that this is abstract)
+        raise NotImplementedError("Sign bytes must be implemented.")
+
+    @abc.abstractproperty
+    def signer_email(self):
+        """Optional[str]: An email address that identifies the signer."""
+        # pylint: disable=missing-raises-doc
+        # (pylint doesn't recognize that this is abstract)
+        raise NotImplementedError("Signer email must be implemented.")
+
+    @abc.abstractproperty
+    def signer(self):
+        """google.auth.crypt.Signer: The signer used to sign bytes."""
+        # pylint: disable=missing-raises-doc
+        # (pylint doesn't recognize that this is abstract)
+        raise NotImplementedError("Signer must be implemented.")
diff --git a/google/auth/crypt/__init__.py b/google/auth/crypt/__init__.py
new file mode 100644
index 0000000..15ac950
--- /dev/null
+++ b/google/auth/crypt/__init__.py
@@ -0,0 +1,100 @@
+# Copyright 2016 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.
+
+"""Cryptography helpers for verifying and signing messages.
+
+The simplest way to verify signatures is using :func:`verify_signature`::
+
+    cert = open('certs.pem').read()
+    valid = crypt.verify_signature(message, signature, cert)
+
+If you're going to verify many messages with the same certificate, you can use
+:class:`RSAVerifier`::
+
+    cert = open('certs.pem').read()
+    verifier = crypt.RSAVerifier.from_string(cert)
+    valid = verifier.verify(message, signature)
+
+To sign messages use :class:`RSASigner` with a private key::
+
+    private_key = open('private_key.pem').read()
+    signer = crypt.RSASigner.from_string(private_key)
+    signature = signer.sign(message)
+
+The code above also works for :class:`ES256Signer` and :class:`ES256Verifier`.
+Note that these two classes are only available if your `cryptography` dependency
+version is at least 1.4.0.
+"""
+
+import six
+
+from google.auth.crypt import base
+from google.auth.crypt import rsa
+
+try:
+    from google.auth.crypt import es256
+except ImportError:  # pragma: NO COVER
+    es256 = None
+
+if es256 is not None:  # pragma: NO COVER
+    __all__ = [
+        "ES256Signer",
+        "ES256Verifier",
+        "RSASigner",
+        "RSAVerifier",
+        "Signer",
+        "Verifier",
+    ]
+else:  # pragma: NO COVER
+    __all__ = ["RSASigner", "RSAVerifier", "Signer", "Verifier"]
+
+
+# Aliases to maintain the v1.0.0 interface, as the crypt module was split
+# into submodules.
+Signer = base.Signer
+Verifier = base.Verifier
+RSASigner = rsa.RSASigner
+RSAVerifier = rsa.RSAVerifier
+
+if es256 is not None:  # pragma: NO COVER
+    ES256Signer = es256.ES256Signer
+    ES256Verifier = es256.ES256Verifier
+
+
+def verify_signature(message, signature, certs, verifier_cls=rsa.RSAVerifier):
+    """Verify an RSA or ECDSA cryptographic signature.
+
+    Checks that the provided ``signature`` was generated from ``bytes`` using
+    the private key associated with the ``cert``.
+
+    Args:
+        message (Union[str, bytes]): The plaintext message.
+        signature (Union[str, bytes]): The cryptographic signature to check.
+        certs (Union[Sequence, str, bytes]): The certificate or certificates
+            to use to check the signature.
+        verifier_cls (Optional[~google.auth.crypt.base.Signer]): Which verifier
+            class to use for verification. This can be used to select different
+            algorithms, such as RSA or ECDSA. Default value is :class:`RSAVerifier`.
+
+    Returns:
+        bool: True if the signature is valid, otherwise False.
+    """
+    if isinstance(certs, (six.text_type, six.binary_type)):
+        certs = [certs]
+
+    for cert in certs:
+        verifier = verifier_cls.from_string(cert)
+        if verifier.verify(message, signature):
+            return True
+    return False
diff --git a/google/auth/crypt/_cryptography_rsa.py b/google/auth/crypt/_cryptography_rsa.py
new file mode 100644
index 0000000..916c9d8
--- /dev/null
+++ b/google/auth/crypt/_cryptography_rsa.py
@@ -0,0 +1,136 @@
+# Copyright 2017 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.
+
+"""RSA verifier and signer that use the ``cryptography`` library.
+
+This is a much faster implementation than the default (in
+``google.auth.crypt._python_rsa``), which depends on the pure-Python
+``rsa`` library.
+"""
+
+import cryptography.exceptions
+from cryptography.hazmat import backends
+from cryptography.hazmat.primitives import hashes
+from cryptography.hazmat.primitives import serialization
+from cryptography.hazmat.primitives.asymmetric import padding
+import cryptography.x509
+
+from google.auth import _helpers
+from google.auth.crypt import base
+
+_CERTIFICATE_MARKER = b"-----BEGIN CERTIFICATE-----"
+_BACKEND = backends.default_backend()
+_PADDING = padding.PKCS1v15()
+_SHA256 = hashes.SHA256()
+
+
+class RSAVerifier(base.Verifier):
+    """Verifies RSA cryptographic signatures using public keys.
+
+    Args:
+        public_key (
+                cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey):
+            The public key used to verify signatures.
+    """
+
+    def __init__(self, public_key):
+        self._pubkey = public_key
+
+    @_helpers.copy_docstring(base.Verifier)
+    def verify(self, message, signature):
+        message = _helpers.to_bytes(message)
+        try:
+            self._pubkey.verify(signature, message, _PADDING, _SHA256)
+            return True
+        except (ValueError, cryptography.exceptions.InvalidSignature):
+            return False
+
+    @classmethod
+    def from_string(cls, public_key):
+        """Construct an Verifier instance from a public key or public
+        certificate string.
+
+        Args:
+            public_key (Union[str, bytes]): The public key in PEM format or the
+                x509 public key certificate.
+
+        Returns:
+            Verifier: The constructed verifier.
+
+        Raises:
+            ValueError: If the public key can't be parsed.
+        """
+        public_key_data = _helpers.to_bytes(public_key)
+
+        if _CERTIFICATE_MARKER in public_key_data:
+            cert = cryptography.x509.load_pem_x509_certificate(
+                public_key_data, _BACKEND
+            )
+            pubkey = cert.public_key()
+
+        else:
+            pubkey = serialization.load_pem_public_key(public_key_data, _BACKEND)
+
+        return cls(pubkey)
+
+
+class RSASigner(base.Signer, base.FromServiceAccountMixin):
+    """Signs messages with an RSA private key.
+
+    Args:
+        private_key (
+                cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
+            The private key to sign with.
+        key_id (str): Optional key ID used to identify this private key. This
+            can be useful to associate the private key with its associated
+            public key or certificate.
+    """
+
+    def __init__(self, private_key, key_id=None):
+        self._key = private_key
+        self._key_id = key_id
+
+    @property
+    @_helpers.copy_docstring(base.Signer)
+    def key_id(self):
+        return self._key_id
+
+    @_helpers.copy_docstring(base.Signer)
+    def sign(self, message):
+        message = _helpers.to_bytes(message)
+        return self._key.sign(message, _PADDING, _SHA256)
+
+    @classmethod
+    def from_string(cls, key, key_id=None):
+        """Construct a RSASigner from a private key in PEM format.
+
+        Args:
+            key (Union[bytes, str]): Private key in PEM format.
+            key_id (str): An optional key id used to identify the private key.
+
+        Returns:
+            google.auth.crypt._cryptography_rsa.RSASigner: The
+            constructed signer.
+
+        Raises:
+            ValueError: If ``key`` is not ``bytes`` or ``str`` (unicode).
+            UnicodeDecodeError: If ``key`` is ``bytes`` but cannot be decoded
+                into a UTF-8 ``str``.
+            ValueError: If ``cryptography`` "Could not deserialize key data."
+        """
+        key = _helpers.to_bytes(key)
+        private_key = serialization.load_pem_private_key(
+            key, password=None, backend=_BACKEND
+        )
+        return cls(private_key, key_id=key_id)
diff --git a/google/auth/crypt/_helpers.py b/google/auth/crypt/_helpers.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/google/auth/crypt/_helpers.py
diff --git a/google/auth/crypt/_python_rsa.py b/google/auth/crypt/_python_rsa.py
new file mode 100644
index 0000000..ec30dd0
--- /dev/null
+++ b/google/auth/crypt/_python_rsa.py
@@ -0,0 +1,173 @@
+# Copyright 2016 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.
+
+"""Pure-Python RSA cryptography implementation.
+
+Uses the ``rsa``, ``pyasn1`` and ``pyasn1_modules`` packages
+to parse PEM files storing PKCS#1 or PKCS#8 keys as well as
+certificates. There is no support for p12 files.
+"""
+
+from __future__ import absolute_import
+
+from pyasn1.codec.der import decoder
+from pyasn1_modules import pem
+from pyasn1_modules.rfc2459 import Certificate
+from pyasn1_modules.rfc5208 import PrivateKeyInfo
+import rsa
+import six
+
+from google.auth import _helpers
+from google.auth.crypt import base
+
+_POW2 = (128, 64, 32, 16, 8, 4, 2, 1)
+_CERTIFICATE_MARKER = b"-----BEGIN CERTIFICATE-----"
+_PKCS1_MARKER = ("-----BEGIN RSA PRIVATE KEY-----", "-----END RSA PRIVATE KEY-----")
+_PKCS8_MARKER = ("-----BEGIN PRIVATE KEY-----", "-----END PRIVATE KEY-----")
+_PKCS8_SPEC = PrivateKeyInfo()
+
+
+def _bit_list_to_bytes(bit_list):
+    """Converts an iterable of 1s and 0s to bytes.
+
+    Combines the list 8 at a time, treating each group of 8 bits
+    as a single byte.
+
+    Args:
+        bit_list (Sequence): Sequence of 1s and 0s.
+
+    Returns:
+        bytes: The decoded bytes.
+    """
+    num_bits = len(bit_list)
+    byte_vals = bytearray()
+    for start in six.moves.xrange(0, num_bits, 8):
+        curr_bits = bit_list[start : start + 8]
+        char_val = sum(val * digit for val, digit in six.moves.zip(_POW2, curr_bits))
+        byte_vals.append(char_val)
+    return bytes(byte_vals)
+
+
+class RSAVerifier(base.Verifier):
+    """Verifies RSA cryptographic signatures using public keys.
+
+    Args:
+        public_key (rsa.key.PublicKey): The public key used to verify
+            signatures.
+    """
+
+    def __init__(self, public_key):
+        self._pubkey = public_key
+
+    @_helpers.copy_docstring(base.Verifier)
+    def verify(self, message, signature):
+        message = _helpers.to_bytes(message)
+        try:
+            return rsa.pkcs1.verify(message, signature, self._pubkey)
+        except (ValueError, rsa.pkcs1.VerificationError):
+            return False
+
+    @classmethod
+    def from_string(cls, public_key):
+        """Construct an Verifier instance from a public key or public
+        certificate string.
+
+        Args:
+            public_key (Union[str, bytes]): The public key in PEM format or the
+                x509 public key certificate.
+
+        Returns:
+            google.auth.crypt._python_rsa.RSAVerifier: The constructed verifier.
+
+        Raises:
+            ValueError: If the public_key can't be parsed.
+        """
+        public_key = _helpers.to_bytes(public_key)
+        is_x509_cert = _CERTIFICATE_MARKER in public_key
+
+        # If this is a certificate, extract the public key info.
+        if is_x509_cert:
+            der = rsa.pem.load_pem(public_key, "CERTIFICATE")
+            asn1_cert, remaining = decoder.decode(der, asn1Spec=Certificate())
+            if remaining != b"":
+                raise ValueError("Unused bytes", remaining)
+
+            cert_info = asn1_cert["tbsCertificate"]["subjectPublicKeyInfo"]
+            key_bytes = _bit_list_to_bytes(cert_info["subjectPublicKey"])
+            pubkey = rsa.PublicKey.load_pkcs1(key_bytes, "DER")
+        else:
+            pubkey = rsa.PublicKey.load_pkcs1(public_key, "PEM")
+        return cls(pubkey)
+
+
+class RSASigner(base.Signer, base.FromServiceAccountMixin):
+    """Signs messages with an RSA private key.
+
+    Args:
+        private_key (rsa.key.PrivateKey): The private key to sign with.
+        key_id (str): Optional key ID used to identify this private key. This
+            can be useful to associate the private key with its associated
+            public key or certificate.
+    """
+
+    def __init__(self, private_key, key_id=None):
+        self._key = private_key
+        self._key_id = key_id
+
+    @property
+    @_helpers.copy_docstring(base.Signer)
+    def key_id(self):
+        return self._key_id
+
+    @_helpers.copy_docstring(base.Signer)
+    def sign(self, message):
+        message = _helpers.to_bytes(message)
+        return rsa.pkcs1.sign(message, self._key, "SHA-256")
+
+    @classmethod
+    def from_string(cls, key, key_id=None):
+        """Construct an Signer instance from a private key in PEM format.
+
+        Args:
+            key (str): Private key in PEM format.
+            key_id (str): An optional key id used to identify the private key.
+
+        Returns:
+            google.auth.crypt.Signer: The constructed signer.
+
+        Raises:
+            ValueError: If the key cannot be parsed as PKCS#1 or PKCS#8 in
+                PEM format.
+        """
+        key = _helpers.from_bytes(key)  # PEM expects str in Python 3
+        marker_id, key_bytes = pem.readPemBlocksFromFile(
+            six.StringIO(key), _PKCS1_MARKER, _PKCS8_MARKER
+        )
+
+        # Key is in pkcs1 format.
+        if marker_id == 0:
+            private_key = rsa.key.PrivateKey.load_pkcs1(key_bytes, format="DER")
+        # Key is in pkcs8.
+        elif marker_id == 1:
+            key_info, remaining = decoder.decode(key_bytes, asn1Spec=_PKCS8_SPEC)
+            if remaining != b"":
+                raise ValueError("Unused bytes", remaining)
+            private_key_info = key_info.getComponentByName("privateKey")
+            private_key = rsa.key.PrivateKey.load_pkcs1(
+                private_key_info.asOctets(), format="DER"
+            )
+        else:
+            raise ValueError("No key could be detected.")
+
+        return cls(private_key, key_id=key_id)
diff --git a/google/auth/crypt/base.py b/google/auth/crypt/base.py
new file mode 100644
index 0000000..c98d5bf
--- /dev/null
+++ b/google/auth/crypt/base.py
@@ -0,0 +1,131 @@
+# Copyright 2016 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.
+
+"""Base classes for cryptographic signers and verifiers."""
+
+import abc
+import io
+import json
+
+import six
+
+
+_JSON_FILE_PRIVATE_KEY = "private_key"
+_JSON_FILE_PRIVATE_KEY_ID = "private_key_id"
+
+
[email protected]_metaclass(abc.ABCMeta)
+class Verifier(object):
+    """Abstract base class for crytographic signature verifiers."""
+
+    @abc.abstractmethod
+    def verify(self, message, signature):
+        """Verifies a message against a cryptographic signature.
+
+        Args:
+            message (Union[str, bytes]): The message to verify.
+            signature (Union[str, bytes]): The cryptography signature to check.
+
+        Returns:
+            bool: True if message was signed by the private key associated
+            with the public key that this object was constructed with.
+        """
+        # pylint: disable=missing-raises-doc,redundant-returns-doc
+        # (pylint doesn't recognize that this is abstract)
+        raise NotImplementedError("Verify must be implemented")
+
+
[email protected]_metaclass(abc.ABCMeta)
+class Signer(object):
+    """Abstract base class for cryptographic signers."""
+
+    @abc.abstractproperty
+    def key_id(self):
+        """Optional[str]: The key ID used to identify this private key."""
+        raise NotImplementedError("Key id must be implemented")
+
+    @abc.abstractmethod
+    def sign(self, message):
+        """Signs a message.
+
+        Args:
+            message (Union[str, bytes]): The message to be signed.
+
+        Returns:
+            bytes: The signature of the message.
+        """
+        # pylint: disable=missing-raises-doc,redundant-returns-doc
+        # (pylint doesn't recognize that this is abstract)
+        raise NotImplementedError("Sign must be implemented")
+
+
[email protected]_metaclass(abc.ABCMeta)
+class FromServiceAccountMixin(object):
+    """Mix-in to enable factory constructors for a Signer."""
+
+    @abc.abstractmethod
+    def from_string(cls, key, key_id=None):
+        """Construct an Signer instance from a private key string.
+
+        Args:
+            key (str): Private key as a string.
+            key_id (str): An optional key id used to identify the private key.
+
+        Returns:
+            google.auth.crypt.Signer: The constructed signer.
+
+        Raises:
+            ValueError: If the key cannot be parsed.
+        """
+        raise NotImplementedError("from_string must be implemented")
+
+    @classmethod
+    def from_service_account_info(cls, info):
+        """Creates a Signer instance instance from a dictionary containing
+        service account info in Google format.
+
+        Args:
+            info (Mapping[str, str]): The service account info in Google
+                format.
+
+        Returns:
+            google.auth.crypt.Signer: The constructed signer.
+
+        Raises:
+            ValueError: If the info is not in the expected format.
+        """
+        if _JSON_FILE_PRIVATE_KEY not in info:
+            raise ValueError(
+                "The private_key field was not found in the service account " "info."
+            )
+
+        return cls.from_string(
+            info[_JSON_FILE_PRIVATE_KEY], info.get(_JSON_FILE_PRIVATE_KEY_ID)
+        )
+
+    @classmethod
+    def from_service_account_file(cls, filename):
+        """Creates a Signer instance from a service account .json file
+        in Google format.
+
+        Args:
+            filename (str): The path to the service account .json file.
+
+        Returns:
+            google.auth.crypt.Signer: The constructed signer.
+        """
+        with io.open(filename, "r", encoding="utf-8") as json_file:
+            data = json.load(json_file)
+
+        return cls.from_service_account_info(data)
diff --git a/google/auth/crypt/es256.py b/google/auth/crypt/es256.py
new file mode 100644
index 0000000..42823a7
--- /dev/null
+++ b/google/auth/crypt/es256.py
@@ -0,0 +1,160 @@
+# Copyright 2017 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""ECDSA (ES256) verifier and signer that use the ``cryptography`` library.
+"""
+
+from cryptography import utils
+import cryptography.exceptions
+from cryptography.hazmat import backends
+from cryptography.hazmat.primitives import hashes
+from cryptography.hazmat.primitives import serialization
+from cryptography.hazmat.primitives.asymmetric import ec
+from cryptography.hazmat.primitives.asymmetric import padding
+from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature
+from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature
+import cryptography.x509
+
+from google.auth import _helpers
+from google.auth.crypt import base
+
+
+_CERTIFICATE_MARKER = b"-----BEGIN CERTIFICATE-----"
+_BACKEND = backends.default_backend()
+_PADDING = padding.PKCS1v15()
+
+
+class ES256Verifier(base.Verifier):
+    """Verifies ECDSA cryptographic signatures using public keys.
+
+    Args:
+        public_key (
+                cryptography.hazmat.primitives.asymmetric.ec.ECDSAPublicKey):
+            The public key used to verify signatures.
+    """
+
+    def __init__(self, public_key):
+        self._pubkey = public_key
+
+    @_helpers.copy_docstring(base.Verifier)
+    def verify(self, message, signature):
+        # First convert (r||s) raw signature to ASN1 encoded signature.
+        sig_bytes = _helpers.to_bytes(signature)
+        if len(sig_bytes) != 64:
+            return False
+        r = (
+            int.from_bytes(sig_bytes[:32], byteorder="big")
+            if _helpers.is_python_3()
+            else utils.int_from_bytes(sig_bytes[:32], byteorder="big")
+        )
+        s = (
+            int.from_bytes(sig_bytes[32:], byteorder="big")
+            if _helpers.is_python_3()
+            else utils.int_from_bytes(sig_bytes[32:], byteorder="big")
+        )
+        asn1_sig = encode_dss_signature(r, s)
+
+        message = _helpers.to_bytes(message)
+        try:
+            self._pubkey.verify(asn1_sig, message, ec.ECDSA(hashes.SHA256()))
+            return True
+        except (ValueError, cryptography.exceptions.InvalidSignature):
+            return False
+
+    @classmethod
+    def from_string(cls, public_key):
+        """Construct an Verifier instance from a public key or public
+        certificate string.
+
+        Args:
+            public_key (Union[str, bytes]): The public key in PEM format or the
+                x509 public key certificate.
+
+        Returns:
+            Verifier: The constructed verifier.
+
+        Raises:
+            ValueError: If the public key can't be parsed.
+        """
+        public_key_data = _helpers.to_bytes(public_key)
+
+        if _CERTIFICATE_MARKER in public_key_data:
+            cert = cryptography.x509.load_pem_x509_certificate(
+                public_key_data, _BACKEND
+            )
+            pubkey = cert.public_key()
+
+        else:
+            pubkey = serialization.load_pem_public_key(public_key_data, _BACKEND)
+
+        return cls(pubkey)
+
+
+class ES256Signer(base.Signer, base.FromServiceAccountMixin):
+    """Signs messages with an ECDSA private key.
+
+    Args:
+        private_key (
+                cryptography.hazmat.primitives.asymmetric.ec.ECDSAPrivateKey):
+            The private key to sign with.
+        key_id (str): Optional key ID used to identify this private key. This
+            can be useful to associate the private key with its associated
+            public key or certificate.
+    """
+
+    def __init__(self, private_key, key_id=None):
+        self._key = private_key
+        self._key_id = key_id
+
+    @property
+    @_helpers.copy_docstring(base.Signer)
+    def key_id(self):
+        return self._key_id
+
+    @_helpers.copy_docstring(base.Signer)
+    def sign(self, message):
+        message = _helpers.to_bytes(message)
+        asn1_signature = self._key.sign(message, ec.ECDSA(hashes.SHA256()))
+
+        # Convert ASN1 encoded signature to (r||s) raw signature.
+        (r, s) = decode_dss_signature(asn1_signature)
+        return (
+            (r.to_bytes(32, byteorder="big") + s.to_bytes(32, byteorder="big"))
+            if _helpers.is_python_3()
+            else (utils.int_to_bytes(r, 32) + utils.int_to_bytes(s, 32))
+        )
+
+    @classmethod
+    def from_string(cls, key, key_id=None):
+        """Construct a RSASigner from a private key in PEM format.
+
+        Args:
+            key (Union[bytes, str]): Private key in PEM format.
+            key_id (str): An optional key id used to identify the private key.
+
+        Returns:
+            google.auth.crypt._cryptography_rsa.RSASigner: The
+            constructed signer.
+
+        Raises:
+            ValueError: If ``key`` is not ``bytes`` or ``str`` (unicode).
+            UnicodeDecodeError: If ``key`` is ``bytes`` but cannot be decoded
+                into a UTF-8 ``str``.
+            ValueError: If ``cryptography`` "Could not deserialize key data."
+        """
+        key = _helpers.to_bytes(key)
+        private_key = serialization.load_pem_private_key(
+            key, password=None, backend=_BACKEND
+        )
+        return cls(private_key, key_id=key_id)
diff --git a/google/auth/crypt/rsa.py b/google/auth/crypt/rsa.py
new file mode 100644
index 0000000..8b2d64c
--- /dev/null
+++ b/google/auth/crypt/rsa.py
@@ -0,0 +1,30 @@
+# Copyright 2017 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.
+
+"""RSA cryptography signer and verifier."""
+
+
+try:
+    # Prefer cryptograph-based RSA implementation.
+    from google.auth.crypt import _cryptography_rsa
+
+    RSASigner = _cryptography_rsa.RSASigner
+    RSAVerifier = _cryptography_rsa.RSAVerifier
+except ImportError:  # pragma: NO COVER
+    # Fallback to pure-python RSA implementation if cryptography is
+    # unavailable.
+    from google.auth.crypt import _python_rsa
+
+    RSASigner = _python_rsa.RSASigner
+    RSAVerifier = _python_rsa.RSAVerifier
diff --git a/google/auth/downscoped.py b/google/auth/downscoped.py
new file mode 100644
index 0000000..a1d7b6e
--- /dev/null
+++ b/google/auth/downscoped.py
@@ -0,0 +1,501 @@
+# 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.
+
+"""Downscoping with Credential Access Boundaries
+
+This module provides the ability to downscope credentials using
+`Downscoping with Credential Access Boundaries`_. This is useful to restrict the
+Identity and Access Management (IAM) permissions that a short-lived credential
+can use.
+
+To downscope permissions of a source credential, a Credential Access Boundary
+that specifies which resources the new credential can access, as well as
+an upper bound on the permissions that are available on each resource, has to
+be defined. A downscoped credential can then be instantiated using the source
+credential and the Credential Access Boundary.
+
+The common pattern of usage is to have a token broker with elevated access
+generate these downscoped credentials from higher access source credentials and
+pass the downscoped short-lived access tokens to a token consumer via some
+secure authenticated channel for limited access to Google Cloud Storage
+resources.
+
+For example, a token broker can be set up on a server in a private network.
+Various workloads (token consumers) in the same network will send authenticated
+requests to that broker for downscoped tokens to access or modify specific google
+cloud storage buckets.
+
+The broker will instantiate downscoped credentials instances that can be used to
+generate short lived downscoped access tokens that can be passed to the token
+consumer. These downscoped access tokens can be injected by the consumer into
+google.oauth2.Credentials and used to initialize a storage client instance to
+access Google Cloud Storage resources with restricted access.
+
+Note: Only Cloud Storage supports Credential Access Boundaries. Other Google
+Cloud services do not support this feature.
+
+.. _Downscoping with Credential Access Boundaries: https://cloud.google.com/iam/docs/downscoping-short-lived-credentials
+"""
+
+import datetime
+
+import six
+
+from google.auth import _helpers
+from google.auth import credentials
+from google.oauth2 import sts
+
+# The maximum number of access boundary rules a Credential Access Boundary can
+# contain.
+_MAX_ACCESS_BOUNDARY_RULES_COUNT = 10
+# The token exchange grant_type used for exchanging credentials.
+_STS_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange"
+# The token exchange requested_token_type. This is always an access_token.
+_STS_REQUESTED_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token"
+# The STS token URL used to exchanged a short lived access token for a downscoped one.
+_STS_TOKEN_URL = "https://sts.googleapis.com/v1/token"
+# The subject token type to use when exchanging a short lived access token for a
+# downscoped token.
+_STS_SUBJECT_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token"
+
+
+class CredentialAccessBoundary(object):
+    """Defines a Credential Access Boundary which contains a list of access boundary
+    rules. Each rule contains information on the resource that the rule applies to,
+    the upper bound of the permissions that are available on that resource and an
+    optional condition to further restrict permissions.
+    """
+
+    def __init__(self, rules=[]):
+        """Instantiates a Credential Access Boundary. A Credential Access Boundary
+        can contain up to 10 access boundary rules.
+
+        Args:
+            rules (Sequence[google.auth.downscoped.AccessBoundaryRule]): The list of
+                access boundary rules limiting the access that a downscoped credential
+                will have.
+        Raises:
+            TypeError: If any of the rules are not a valid type.
+            ValueError: If the provided rules exceed the maximum allowed.
+        """
+        self.rules = rules
+
+    @property
+    def rules(self):
+        """Returns the list of access boundary rules defined on the Credential
+        Access Boundary.
+
+        Returns:
+            Tuple[google.auth.downscoped.AccessBoundaryRule, ...]: The list of access
+                boundary rules defined on the Credential Access Boundary. These are returned
+                as an immutable tuple to prevent modification.
+        """
+        return tuple(self._rules)
+
+    @rules.setter
+    def rules(self, value):
+        """Updates the current rules on the Credential Access Boundary. This will overwrite
+        the existing set of rules.
+
+        Args:
+            value (Sequence[google.auth.downscoped.AccessBoundaryRule]): The list of
+                access boundary rules limiting the access that a downscoped credential
+                will have.
+        Raises:
+            TypeError: If any of the rules are not a valid type.
+            ValueError: If the provided rules exceed the maximum allowed.
+        """
+        if len(value) > _MAX_ACCESS_BOUNDARY_RULES_COUNT:
+            raise ValueError(
+                "Credential access boundary rules can have a maximum of {} rules.".format(
+                    _MAX_ACCESS_BOUNDARY_RULES_COUNT
+                )
+            )
+        for access_boundary_rule in value:
+            if not isinstance(access_boundary_rule, AccessBoundaryRule):
+                raise TypeError(
+                    "List of rules provided do not contain a valid 'google.auth.downscoped.AccessBoundaryRule'."
+                )
+        # Make a copy of the original list.
+        self._rules = list(value)
+
+    def add_rule(self, rule):
+        """Adds a single access boundary rule to the existing rules.
+
+        Args:
+            rule (google.auth.downscoped.AccessBoundaryRule): The access boundary rule,
+                limiting the access that a downscoped credential will have, to be added to
+                the existing rules.
+        Raises:
+            TypeError: If any of the rules are not a valid type.
+            ValueError: If the provided rules exceed the maximum allowed.
+        """
+        if len(self.rules) == _MAX_ACCESS_BOUNDARY_RULES_COUNT:
+            raise ValueError(
+                "Credential access boundary rules can have a maximum of {} rules.".format(
+                    _MAX_ACCESS_BOUNDARY_RULES_COUNT
+                )
+            )
+        if not isinstance(rule, AccessBoundaryRule):
+            raise TypeError(
+                "The provided rule does not contain a valid 'google.auth.downscoped.AccessBoundaryRule'."
+            )
+        self._rules.append(rule)
+
+    def to_json(self):
+        """Generates the dictionary representation of the Credential Access Boundary.
+        This uses the format expected by the Security Token Service API as documented in
+        `Defining a Credential Access Boundary`_.
+
+        .. _Defining a Credential Access Boundary:
+            https://cloud.google.com/iam/docs/downscoping-short-lived-credentials#define-boundary
+
+        Returns:
+            Mapping: Credential Access Boundary Rule represented in a dictionary object.
+        """
+        rules = []
+        for access_boundary_rule in self.rules:
+            rules.append(access_boundary_rule.to_json())
+
+        return {"accessBoundary": {"accessBoundaryRules": rules}}
+
+
+class AccessBoundaryRule(object):
+    """Defines an access boundary rule which contains information on the resource that
+    the rule applies to, the upper bound of the permissions that are available on that
+    resource and an optional condition to further restrict permissions.
+    """
+
+    def __init__(
+        self, available_resource, available_permissions, availability_condition=None
+    ):
+        """Instantiates a single access boundary rule.
+
+        Args:
+            available_resource (str): The full resource name of the Cloud Storage bucket
+                that the rule applies to. Use the format
+                "//storage.googleapis.com/projects/_/buckets/bucket-name".
+            available_permissions (Sequence[str]): A list defining the upper bound that
+                the downscoped token will have on the available permissions for the
+                resource. Each value is the identifier for an IAM predefined role or
+                custom role, with the prefix "inRole:". For example:
+                "inRole:roles/storage.objectViewer".
+                Only the permissions in these roles will be available.
+            availability_condition (Optional[google.auth.downscoped.AvailabilityCondition]):
+                Optional condition that restricts the availability of permissions to
+                specific Cloud Storage objects.
+
+        Raises:
+            TypeError: If any of the parameters are not of the expected types.
+            ValueError: If any of the parameters are not of the expected values.
+        """
+        self.available_resource = available_resource
+        self.available_permissions = available_permissions
+        self.availability_condition = availability_condition
+
+    @property
+    def available_resource(self):
+        """Returns the current available resource.
+
+        Returns:
+           str: The current available resource.
+        """
+        return self._available_resource
+
+    @available_resource.setter
+    def available_resource(self, value):
+        """Updates the current available resource.
+
+        Args:
+            value (str): The updated value of the available resource.
+
+        Raises:
+            TypeError: If the value is not a string.
+        """
+        if not isinstance(value, six.string_types):
+            raise TypeError("The provided available_resource is not a string.")
+        self._available_resource = value
+
+    @property
+    def available_permissions(self):
+        """Returns the current available permissions.
+
+        Returns:
+           Tuple[str, ...]: The current available permissions. These are returned
+               as an immutable tuple to prevent modification.
+        """
+        return tuple(self._available_permissions)
+
+    @available_permissions.setter
+    def available_permissions(self, value):
+        """Updates the current available permissions.
+
+        Args:
+            value (Sequence[str]): The updated value of the available permissions.
+
+        Raises:
+            TypeError: If the value is not a list of strings.
+            ValueError: If the value is not valid.
+        """
+        for available_permission in value:
+            if not isinstance(available_permission, six.string_types):
+                raise TypeError(
+                    "Provided available_permissions are not a list of strings."
+                )
+            if available_permission.find("inRole:") != 0:
+                raise ValueError(
+                    "available_permissions must be prefixed with 'inRole:'."
+                )
+        # Make a copy of the original list.
+        self._available_permissions = list(value)
+
+    @property
+    def availability_condition(self):
+        """Returns the current availability condition.
+
+        Returns:
+           Optional[google.auth.downscoped.AvailabilityCondition]: The current
+               availability condition.
+        """
+        return self._availability_condition
+
+    @availability_condition.setter
+    def availability_condition(self, value):
+        """Updates the current availability condition.
+
+        Args:
+            value (Optional[google.auth.downscoped.AvailabilityCondition]): The updated
+                value of the availability condition.
+
+        Raises:
+            TypeError: If the value is not of type google.auth.downscoped.AvailabilityCondition
+                or None.
+        """
+        if not isinstance(value, AvailabilityCondition) and value is not None:
+            raise TypeError(
+                "The provided availability_condition is not a 'google.auth.downscoped.AvailabilityCondition' or None."
+            )
+        self._availability_condition = value
+
+    def to_json(self):
+        """Generates the dictionary representation of the access boundary rule.
+        This uses the format expected by the Security Token Service API as documented in
+        `Defining a Credential Access Boundary`_.
+
+        .. _Defining a Credential Access Boundary:
+            https://cloud.google.com/iam/docs/downscoping-short-lived-credentials#define-boundary
+
+        Returns:
+            Mapping: The access boundary rule represented in a dictionary object.
+        """
+        json = {
+            "availablePermissions": list(self.available_permissions),
+            "availableResource": self.available_resource,
+        }
+        if self.availability_condition:
+            json["availabilityCondition"] = self.availability_condition.to_json()
+        return json
+
+
+class AvailabilityCondition(object):
+    """An optional condition that can be used as part of a Credential Access Boundary
+    to further restrict permissions."""
+
+    def __init__(self, expression, title=None, description=None):
+        """Instantiates an availability condition using the provided expression and
+        optional title or description.
+
+        Args:
+            expression (str): A condition expression that specifies the Cloud Storage
+                objects where permissions are available. For example, this expression
+                makes permissions available for objects whose name starts with "customer-a":
+                "resource.name.startsWith('projects/_/buckets/example-bucket/objects/customer-a')"
+            title (Optional[str]): An optional short string that identifies the purpose of
+                the condition.
+            description (Optional[str]): Optional details about the purpose of the condition.
+
+        Raises:
+            TypeError: If any of the parameters are not of the expected types.
+            ValueError: If any of the parameters are not of the expected values.
+        """
+        self.expression = expression
+        self.title = title
+        self.description = description
+
+    @property
+    def expression(self):
+        """Returns the current condition expression.
+
+        Returns:
+           str: The current conditon expression.
+        """
+        return self._expression
+
+    @expression.setter
+    def expression(self, value):
+        """Updates the current condition expression.
+
+        Args:
+            value (str): The updated value of the condition expression.
+
+        Raises:
+            TypeError: If the value is not of type string.
+        """
+        if not isinstance(value, six.string_types):
+            raise TypeError("The provided expression is not a string.")
+        self._expression = value
+
+    @property
+    def title(self):
+        """Returns the current title.
+
+        Returns:
+           Optional[str]: The current title.
+        """
+        return self._title
+
+    @title.setter
+    def title(self, value):
+        """Updates the current title.
+
+        Args:
+            value (Optional[str]): The updated value of the title.
+
+        Raises:
+            TypeError: If the value is not of type string or None.
+        """
+        if not isinstance(value, six.string_types) and value is not None:
+            raise TypeError("The provided title is not a string or None.")
+        self._title = value
+
+    @property
+    def description(self):
+        """Returns the current description.
+
+        Returns:
+           Optional[str]: The current description.
+        """
+        return self._description
+
+    @description.setter
+    def description(self, value):
+        """Updates the current description.
+
+        Args:
+            value (Optional[str]): The updated value of the description.
+
+        Raises:
+            TypeError: If the value is not of type string or None.
+        """
+        if not isinstance(value, six.string_types) and value is not None:
+            raise TypeError("The provided description is not a string or None.")
+        self._description = value
+
+    def to_json(self):
+        """Generates the dictionary representation of the availability condition.
+        This uses the format expected by the Security Token Service API as documented in
+        `Defining a Credential Access Boundary`_.
+
+        .. _Defining a Credential Access Boundary:
+            https://cloud.google.com/iam/docs/downscoping-short-lived-credentials#define-boundary
+
+        Returns:
+            Mapping[str, str]: The availability condition represented in a dictionary
+                object.
+        """
+        json = {"expression": self.expression}
+        if self.title:
+            json["title"] = self.title
+        if self.description:
+            json["description"] = self.description
+        return json
+
+
+class Credentials(credentials.CredentialsWithQuotaProject):
+    """Defines a set of Google credentials that are downscoped from an existing set
+    of Google OAuth2 credentials. This is useful to restrict the Identity and Access
+    Management (IAM) permissions that a short-lived credential can use.
+    The common pattern of usage is to have a token broker with elevated access
+    generate these downscoped credentials from higher access source credentials and
+    pass the downscoped short-lived access tokens to a token consumer via some
+    secure authenticated channel for limited access to Google Cloud Storage
+    resources.
+    """
+
+    def __init__(
+        self, source_credentials, credential_access_boundary, quota_project_id=None
+    ):
+        """Instantiates a downscoped credentials object using the provided source
+        credentials and credential access boundary rules.
+        To downscope permissions of a source credential, a Credential Access Boundary
+        that specifies which resources the new credential can access, as well as an
+        upper bound on the permissions that are available on each resource, has to be
+        defined. A downscoped credential can then be instantiated using the source
+        credential and the Credential Access Boundary.
+
+        Args:
+            source_credentials (google.auth.credentials.Credentials): The source credentials
+                to be downscoped based on the provided Credential Access Boundary rules.
+            credential_access_boundary (google.auth.downscoped.CredentialAccessBoundary):
+                The Credential Access Boundary which contains a list of access boundary
+                rules. Each rule contains information on the resource that the rule applies to,
+                the upper bound of the permissions that are available on that resource and an
+                optional condition to further restrict permissions.
+            quota_project_id (Optional[str]): The optional quota project ID.
+        Raises:
+            google.auth.exceptions.RefreshError: If the source credentials
+                return an error on token refresh.
+            google.auth.exceptions.OAuthError: If the STS token exchange
+                endpoint returned an error during downscoped token generation.
+        """
+
+        super(Credentials, self).__init__()
+        self._source_credentials = source_credentials
+        self._credential_access_boundary = credential_access_boundary
+        self._quota_project_id = quota_project_id
+        self._sts_client = sts.Client(_STS_TOKEN_URL)
+
+    @_helpers.copy_docstring(credentials.Credentials)
+    def refresh(self, request):
+        # Generate an access token from the source credentials.
+        self._source_credentials.refresh(request)
+        now = _helpers.utcnow()
+        # Exchange the access token for a downscoped access token.
+        response_data = self._sts_client.exchange_token(
+            request=request,
+            grant_type=_STS_GRANT_TYPE,
+            subject_token=self._source_credentials.token,
+            subject_token_type=_STS_SUBJECT_TOKEN_TYPE,
+            requested_token_type=_STS_REQUESTED_TOKEN_TYPE,
+            additional_options=self._credential_access_boundary.to_json(),
+        )
+        self.token = response_data.get("access_token")
+        # For downscoping CAB flow, the STS endpoint may not return the expiration
+        # field for some flows. The generated downscoped token should always have
+        # the same expiration time as the source credentials. When no expires_in
+        # field is returned in the response, we can just get the expiration time
+        # from the source credentials.
+        if response_data.get("expires_in"):
+            lifetime = datetime.timedelta(seconds=response_data.get("expires_in"))
+            self.expiry = now + lifetime
+        else:
+            self.expiry = self._source_credentials.expiry
+
+    @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
+    def with_quota_project(self, quota_project_id):
+        return self.__class__(
+            self._source_credentials,
+            self._credential_access_boundary,
+            quota_project_id=quota_project_id,
+        )
diff --git a/google/auth/environment_vars.py b/google/auth/environment_vars.py
new file mode 100644
index 0000000..c076dc5
--- /dev/null
+++ b/google/auth/environment_vars.py
@@ -0,0 +1,80 @@
+# Copyright 2016 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.
+
+"""Environment variables used by :mod:`google.auth`."""
+
+
+PROJECT = "GOOGLE_CLOUD_PROJECT"
+"""Environment variable defining default project.
+
+This used by :func:`google.auth.default` to explicitly set a project ID. This
+environment variable is also used by the Google Cloud Python Library.
+"""
+
+LEGACY_PROJECT = "GCLOUD_PROJECT"
+"""Previously used environment variable defining the default project.
+
+This environment variable is used instead of the current one in some
+situations (such as Google App Engine).
+"""
+
+CREDENTIALS = "GOOGLE_APPLICATION_CREDENTIALS"
+"""Environment variable defining the location of Google application default
+credentials."""
+
+# The environment variable name which can replace ~/.config if set.
+CLOUD_SDK_CONFIG_DIR = "CLOUDSDK_CONFIG"
+"""Environment variable defines the location of Google Cloud SDK's config
+files."""
+
+# These two variables allow for customization of the addresses used when
+# contacting the GCE metadata service.
+GCE_METADATA_HOST = "GCE_METADATA_HOST"
+"""Environment variable providing an alternate hostname or host:port to be
+used for GCE metadata requests.
+
+This environment variable was originally named GCE_METADATA_ROOT. The system will
+check this environemnt variable first; should there be no value present,
+the system will fall back to the old variable.
+"""
+
+GCE_METADATA_ROOT = "GCE_METADATA_ROOT"
+"""Old environment variable for GCE_METADATA_HOST."""
+
+GCE_METADATA_IP = "GCE_METADATA_IP"
+"""Environment variable providing an alternate ip:port to be used for ip-only
+GCE metadata requests."""
+
+GOOGLE_API_USE_CLIENT_CERTIFICATE = "GOOGLE_API_USE_CLIENT_CERTIFICATE"
+"""Environment variable controlling whether to use client certificate or not.
+
+The default value is false. Users have to explicitly set this value to true
+in order to use client certificate to establish a mutual TLS channel."""
+
+LEGACY_APPENGINE_RUNTIME = "APPENGINE_RUNTIME"
+"""Gen1 environment variable defining the App Engine Runtime.
+
+Used to distinguish between GAE gen1 and GAE gen2+.
+"""
+
+# AWS environment variables used with AWS workload identity pools to retrieve
+# AWS security credentials and the AWS region needed to create a serialized
+# signed requests to the AWS STS GetCalledIdentity API that can be exchanged
+# for a Google access tokens via the GCP STS endpoint.
+# When not available the AWS metadata server is used to retrieve these values.
+AWS_ACCESS_KEY_ID = "AWS_ACCESS_KEY_ID"
+AWS_SECRET_ACCESS_KEY = "AWS_SECRET_ACCESS_KEY"
+AWS_SESSION_TOKEN = "AWS_SESSION_TOKEN"
+AWS_REGION = "AWS_REGION"
+AWS_DEFAULT_REGION = "AWS_DEFAULT_REGION"
diff --git a/google/auth/exceptions.py b/google/auth/exceptions.py
new file mode 100644
index 0000000..e9e7377
--- /dev/null
+++ b/google/auth/exceptions.py
@@ -0,0 +1,63 @@
+# Copyright 2016 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.
+
+"""Exceptions used in the google.auth package."""
+
+
+class GoogleAuthError(Exception):
+    """Base class for all google.auth errors."""
+
+
+class TransportError(GoogleAuthError):
+    """Used to indicate an error occurred during an HTTP request."""
+
+
+class RefreshError(GoogleAuthError):
+    """Used to indicate that an refreshing the credentials' access token
+    failed."""
+
+
+class UserAccessTokenError(GoogleAuthError):
+    """Used to indicate ``gcloud auth print-access-token`` command failed."""
+
+
+class DefaultCredentialsError(GoogleAuthError):
+    """Used to indicate that acquiring default credentials failed."""
+
+
+class MutualTLSChannelError(GoogleAuthError):
+    """Used to indicate that mutual TLS channel creation is failed, or mutual
+    TLS channel credentials is missing or invalid."""
+
+
+class ClientCertError(GoogleAuthError):
+    """Used to indicate that client certificate is missing or invalid."""
+
+
+class OAuthError(GoogleAuthError):
+    """Used to indicate an error occurred during an OAuth related HTTP
+    request."""
+
+
+class ReauthFailError(RefreshError):
+    """An exception for when reauth failed."""
+
+    def __init__(self, message=None):
+        super(ReauthFailError, self).__init__(
+            "Reauthentication failed. {0}".format(message)
+        )
+
+
+class ReauthSamlChallengeFailError(ReauthFailError):
+    """An exception for SAML reauth challenge failures."""
diff --git a/google/auth/external_account.py b/google/auth/external_account.py
new file mode 100644
index 0000000..cbd0baf
--- /dev/null
+++ b/google/auth/external_account.py
@@ -0,0 +1,415 @@
+# 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.
+
+"""External Account Credentials.
+
+This module provides credentials that exchange workload identity pool external
+credentials for Google access tokens. This facilitates accessing Google Cloud
+Platform resources from on-prem and non-Google Cloud platforms (e.g. AWS,
+Microsoft Azure, OIDC identity providers), using native credentials retrieved
+from the current environment without the need to copy, save and manage
+long-lived service account credentials.
+
+Specifically, this is intended to use access tokens acquired using the GCP STS
+token exchange endpoint following the `OAuth 2.0 Token Exchange`_ spec.
+
+.. _OAuth 2.0 Token Exchange: https://tools.ietf.org/html/rfc8693
+"""
+
+import abc
+import copy
+import datetime
+import json
+import re
+
+import six
+
+from google.auth import _helpers
+from google.auth import credentials
+from google.auth import exceptions
+from google.auth import impersonated_credentials
+from google.oauth2 import sts
+from google.oauth2 import utils
+
+# External account JSON type identifier.
+_EXTERNAL_ACCOUNT_JSON_TYPE = "external_account"
+# The token exchange grant_type used for exchanging credentials.
+_STS_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange"
+# The token exchange requested_token_type. This is always an access_token.
+_STS_REQUESTED_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token"
+# Cloud resource manager URL used to retrieve project information.
+_CLOUD_RESOURCE_MANAGER = "https://cloudresourcemanager.googleapis.com/v1/projects/"
+
+
[email protected]_metaclass(abc.ABCMeta)
+class Credentials(credentials.Scoped, credentials.CredentialsWithQuotaProject):
+    """Base class for all external account credentials.
+
+    This is used to instantiate Credentials for exchanging external account
+    credentials for Google access token and authorizing requests to Google APIs.
+    The base class implements the common logic for exchanging external account
+    credentials for Google access tokens.
+    """
+
+    def __init__(
+        self,
+        audience,
+        subject_token_type,
+        token_url,
+        credential_source,
+        service_account_impersonation_url=None,
+        client_id=None,
+        client_secret=None,
+        quota_project_id=None,
+        scopes=None,
+        default_scopes=None,
+        workforce_pool_user_project=None,
+    ):
+        """Instantiates an external account credentials object.
+
+        Args:
+            audience (str): The STS audience field.
+            subject_token_type (str): The subject token type.
+            token_url (str): The STS endpoint URL.
+            credential_source (Mapping): The credential source dictionary.
+            service_account_impersonation_url (Optional[str]): The optional service account
+                impersonation generateAccessToken URL.
+            client_id (Optional[str]): The optional client ID.
+            client_secret (Optional[str]): The optional client secret.
+            quota_project_id (Optional[str]): The optional quota project ID.
+            scopes (Optional[Sequence[str]]): Optional scopes to request during the
+                authorization grant.
+            default_scopes (Optional[Sequence[str]]): Default scopes passed by a
+                Google client library. Use 'scopes' for user-defined scopes.
+            workforce_pool_user_project (Optona[str]): The optional workforce pool user
+                project number when the credential corresponds to a workforce pool and not
+                a workload identity pool. The underlying principal must still have
+                serviceusage.services.use IAM permission to use the project for
+                billing/quota.
+        Raises:
+            google.auth.exceptions.RefreshError: If the generateAccessToken
+                endpoint returned an error.
+        """
+        super(Credentials, self).__init__()
+        self._audience = audience
+        self._subject_token_type = subject_token_type
+        self._token_url = token_url
+        self._credential_source = credential_source
+        self._service_account_impersonation_url = service_account_impersonation_url
+        self._client_id = client_id
+        self._client_secret = client_secret
+        self._quota_project_id = quota_project_id
+        self._scopes = scopes
+        self._default_scopes = default_scopes
+        self._workforce_pool_user_project = workforce_pool_user_project
+
+        if self._client_id:
+            self._client_auth = utils.ClientAuthentication(
+                utils.ClientAuthType.basic, self._client_id, self._client_secret
+            )
+        else:
+            self._client_auth = None
+        self._sts_client = sts.Client(self._token_url, self._client_auth)
+
+        if self._service_account_impersonation_url:
+            self._impersonated_credentials = self._initialize_impersonated_credentials()
+        else:
+            self._impersonated_credentials = None
+        self._project_id = None
+
+        if not self.is_workforce_pool and self._workforce_pool_user_project:
+            # Workload identity pools do not support workforce pool user projects.
+            raise ValueError(
+                "workforce_pool_user_project should not be set for non-workforce pool "
+                "credentials"
+            )
+
+    @property
+    def info(self):
+        """Generates the dictionary representation of the current credentials.
+
+        Returns:
+            Mapping: The dictionary representation of the credentials. This is the
+                reverse of "from_info" defined on the subclasses of this class. It is
+                useful for serializing the current credentials so it can deserialized
+                later.
+        """
+        config_info = {
+            "type": _EXTERNAL_ACCOUNT_JSON_TYPE,
+            "audience": self._audience,
+            "subject_token_type": self._subject_token_type,
+            "token_url": self._token_url,
+            "service_account_impersonation_url": self._service_account_impersonation_url,
+            "credential_source": copy.deepcopy(self._credential_source),
+            "quota_project_id": self._quota_project_id,
+            "client_id": self._client_id,
+            "client_secret": self._client_secret,
+            "workforce_pool_user_project": self._workforce_pool_user_project,
+        }
+        return {key: value for key, value in config_info.items() if value is not None}
+
+    @property
+    def service_account_email(self):
+        """Returns the service account email if service account impersonation is used.
+
+        Returns:
+            Optional[str]: The service account email if impersonation is used. Otherwise
+                None is returned.
+        """
+        if self._service_account_impersonation_url:
+            # Parse email from URL. The formal looks as follows:
+            # https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/[email protected]:generateAccessToken
+            url = self._service_account_impersonation_url
+            start_index = url.rfind("/")
+            end_index = url.find(":generateAccessToken")
+            if start_index != -1 and end_index != -1 and start_index < end_index:
+                start_index = start_index + 1
+                return url[start_index:end_index]
+        return None
+
+    @property
+    def is_user(self):
+        """Returns whether the credentials represent a user (True) or workload (False).
+        Workloads behave similarly to service accounts. Currently workloads will use
+        service account impersonation but will eventually not require impersonation.
+        As a result, this property is more reliable than the service account email
+        property in determining if the credentials represent a user or workload.
+
+        Returns:
+            bool: True if the credentials represent a user. False if they represent a
+                workload.
+        """
+        # If service account impersonation is used, the credentials will always represent a
+        # service account.
+        if self._service_account_impersonation_url:
+            return False
+        return self.is_workforce_pool
+
+    @property
+    def is_workforce_pool(self):
+        """Returns whether the credentials represent a workforce pool (True) or
+        workload (False) based on the credentials' audience.
+
+        This will also return True for impersonated workforce pool credentials.
+
+        Returns:
+            bool: True if the credentials represent a workforce pool. False if they
+                represent a workload.
+        """
+        # Workforce pools representing users have the following audience format:
+        # //iam.googleapis.com/locations/$location/workforcePools/$poolId/providers/$providerId
+        p = re.compile(r"//iam\.googleapis\.com/locations/[^/]+/workforcePools/")
+        return p.match(self._audience or "") is not None
+
+    @property
+    def requires_scopes(self):
+        """Checks if the credentials requires scopes.
+
+        Returns:
+            bool: True if there are no scopes set otherwise False.
+        """
+        return not self._scopes and not self._default_scopes
+
+    @property
+    def project_number(self):
+        """Optional[str]: The project number corresponding to the workload identity pool."""
+
+        # STS audience pattern:
+        # //iam.googleapis.com/projects/$PROJECT_NUMBER/locations/...
+        components = self._audience.split("/")
+        try:
+            project_index = components.index("projects")
+            if project_index + 1 < len(components):
+                return components[project_index + 1] or None
+        except ValueError:
+            return None
+
+    @_helpers.copy_docstring(credentials.Scoped)
+    def with_scopes(self, scopes, default_scopes=None):
+        d = dict(
+            audience=self._audience,
+            subject_token_type=self._subject_token_type,
+            token_url=self._token_url,
+            credential_source=self._credential_source,
+            service_account_impersonation_url=self._service_account_impersonation_url,
+            client_id=self._client_id,
+            client_secret=self._client_secret,
+            quota_project_id=self._quota_project_id,
+            scopes=scopes,
+            default_scopes=default_scopes,
+            workforce_pool_user_project=self._workforce_pool_user_project,
+        )
+        if not self.is_workforce_pool:
+            d.pop("workforce_pool_user_project")
+        return self.__class__(**d)
+
+    @abc.abstractmethod
+    def retrieve_subject_token(self, request):
+        """Retrieves the subject token using the credential_source object.
+
+        Args:
+            request (google.auth.transport.Request): A callable used to make
+                HTTP requests.
+        Returns:
+            str: The retrieved subject token.
+        """
+        # pylint: disable=missing-raises-doc
+        # (pylint doesn't recognize that this is abstract)
+        raise NotImplementedError("retrieve_subject_token must be implemented")
+
+    def get_project_id(self, request):
+        """Retrieves the project ID corresponding to the workload identity or workforce pool.
+        For workforce pool credentials, it returns the project ID corresponding to
+        the workforce_pool_user_project.
+
+        When not determinable, None is returned.
+
+        This is introduced to support the current pattern of using the Auth library:
+
+            credentials, project_id = google.auth.default()
+
+        The resource may not have permission (resourcemanager.projects.get) to
+        call this API or the required scopes may not be selected:
+        https://cloud.google.com/resource-manager/reference/rest/v1/projects/get#authorization-scopes
+
+        Args:
+            request (google.auth.transport.Request): A callable used to make
+                HTTP requests.
+        Returns:
+            Optional[str]: The project ID corresponding to the workload identity pool
+                or workforce pool if determinable.
+        """
+        if self._project_id:
+            # If already retrieved, return the cached project ID value.
+            return self._project_id
+        scopes = self._scopes if self._scopes is not None else self._default_scopes
+        # Scopes are required in order to retrieve a valid access token.
+        project_number = self.project_number or self._workforce_pool_user_project
+        if project_number and scopes:
+            headers = {}
+            url = _CLOUD_RESOURCE_MANAGER + project_number
+            self.before_request(request, "GET", url, headers)
+            response = request(url=url, method="GET", headers=headers)
+
+            response_body = (
+                response.data.decode("utf-8")
+                if hasattr(response.data, "decode")
+                else response.data
+            )
+            response_data = json.loads(response_body)
+
+            if response.status == 200:
+                # Cache result as this field is immutable.
+                self._project_id = response_data.get("projectId")
+                return self._project_id
+
+        return None
+
+    @_helpers.copy_docstring(credentials.Credentials)
+    def refresh(self, request):
+        scopes = self._scopes if self._scopes is not None else self._default_scopes
+        if self._impersonated_credentials:
+            self._impersonated_credentials.refresh(request)
+            self.token = self._impersonated_credentials.token
+            self.expiry = self._impersonated_credentials.expiry
+        else:
+            now = _helpers.utcnow()
+            additional_options = None
+            # Do not pass workforce_pool_user_project when client authentication
+            # is used. The client ID is sufficient for determining the user project.
+            if self._workforce_pool_user_project and not self._client_id:
+                additional_options = {"userProject": self._workforce_pool_user_project}
+            response_data = self._sts_client.exchange_token(
+                request=request,
+                grant_type=_STS_GRANT_TYPE,
+                subject_token=self.retrieve_subject_token(request),
+                subject_token_type=self._subject_token_type,
+                audience=self._audience,
+                scopes=scopes,
+                requested_token_type=_STS_REQUESTED_TOKEN_TYPE,
+                additional_options=additional_options,
+            )
+            self.token = response_data.get("access_token")
+            lifetime = datetime.timedelta(seconds=response_data.get("expires_in"))
+            self.expiry = now + lifetime
+
+    @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
+    def with_quota_project(self, quota_project_id):
+        # Return copy of instance with the provided quota project ID.
+        d = dict(
+            audience=self._audience,
+            subject_token_type=self._subject_token_type,
+            token_url=self._token_url,
+            credential_source=self._credential_source,
+            service_account_impersonation_url=self._service_account_impersonation_url,
+            client_id=self._client_id,
+            client_secret=self._client_secret,
+            quota_project_id=quota_project_id,
+            scopes=self._scopes,
+            default_scopes=self._default_scopes,
+            workforce_pool_user_project=self._workforce_pool_user_project,
+        )
+        if not self.is_workforce_pool:
+            d.pop("workforce_pool_user_project")
+        return self.__class__(**d)
+
+    def _initialize_impersonated_credentials(self):
+        """Generates an impersonated credentials.
+
+        For more details, see `projects.serviceAccounts.generateAccessToken`_.
+
+        .. _projects.serviceAccounts.generateAccessToken: https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/generateAccessToken
+
+        Returns:
+            impersonated_credentials.Credential: The impersonated credentials
+                object.
+
+        Raises:
+            google.auth.exceptions.RefreshError: If the generateAccessToken
+                endpoint returned an error.
+        """
+        # Return copy of instance with no service account impersonation.
+        d = dict(
+            audience=self._audience,
+            subject_token_type=self._subject_token_type,
+            token_url=self._token_url,
+            credential_source=self._credential_source,
+            service_account_impersonation_url=None,
+            client_id=self._client_id,
+            client_secret=self._client_secret,
+            quota_project_id=self._quota_project_id,
+            scopes=self._scopes,
+            default_scopes=self._default_scopes,
+            workforce_pool_user_project=self._workforce_pool_user_project,
+        )
+        if not self.is_workforce_pool:
+            d.pop("workforce_pool_user_project")
+        source_credentials = self.__class__(**d)
+
+        # Determine target_principal.
+        target_principal = self.service_account_email
+        if not target_principal:
+            raise exceptions.RefreshError(
+                "Unable to determine target principal from service account impersonation URL."
+            )
+
+        scopes = self._scopes if self._scopes is not None else self._default_scopes
+        # Initialize and return impersonated credentials.
+        return impersonated_credentials.Credentials(
+            source_credentials=source_credentials,
+            target_principal=target_principal,
+            target_scopes=scopes,
+            quota_project_id=self._quota_project_id,
+            iam_endpoint_override=self._service_account_impersonation_url,
+        )
diff --git a/google/auth/iam.py b/google/auth/iam.py
new file mode 100644
index 0000000..5d63dc5
--- /dev/null
+++ b/google/auth/iam.py
@@ -0,0 +1,100 @@
+# Copyright 2017 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.
+
+"""Tools for using the Google `Cloud Identity and Access Management (IAM)
+API`_'s auth-related functionality.
+
+.. _Cloud Identity and Access Management (IAM) API:
+    https://cloud.google.com/iam/docs/
+"""
+
+import base64
+import json
+
+from six.moves import http_client
+
+from google.auth import _helpers
+from google.auth import crypt
+from google.auth import exceptions
+
+_IAM_API_ROOT_URI = "https://iamcredentials.googleapis.com/v1"
+_SIGN_BLOB_URI = _IAM_API_ROOT_URI + "/projects/-/serviceAccounts/{}:signBlob?alt=json"
+
+
+class Signer(crypt.Signer):
+    """Signs messages using the IAM `signBlob API`_.
+
+    This is useful when you need to sign bytes but do not have access to the
+    credential's private key file.
+
+    .. _signBlob API:
+        https://cloud.google.com/iam/reference/rest/v1/projects.serviceAccounts
+        /signBlob
+    """
+
+    def __init__(self, request, credentials, service_account_email):
+        """
+        Args:
+            request (google.auth.transport.Request): The object used to make
+                HTTP requests.
+            credentials (google.auth.credentials.Credentials): The credentials
+                that will be used to authenticate the request to the IAM API.
+                The credentials must have of one the following scopes:
+
+                - https://www.googleapis.com/auth/iam
+                - https://www.googleapis.com/auth/cloud-platform
+            service_account_email (str): The service account email identifying
+                which service account to use to sign bytes. Often, this can
+                be the same as the service account email in the given
+                credentials.
+        """
+        self._request = request
+        self._credentials = credentials
+        self._service_account_email = service_account_email
+
+    def _make_signing_request(self, message):
+        """Makes a request to the API signBlob API."""
+        message = _helpers.to_bytes(message)
+
+        method = "POST"
+        url = _SIGN_BLOB_URI.format(self._service_account_email)
+        headers = {"Content-Type": "application/json"}
+        body = json.dumps(
+            {"payload": base64.b64encode(message).decode("utf-8")}
+        ).encode("utf-8")
+
+        self._credentials.before_request(self._request, method, url, headers)
+        response = self._request(url=url, method=method, body=body, headers=headers)
+
+        if response.status != http_client.OK:
+            raise exceptions.TransportError(
+                "Error calling the IAM signBlob API: {}".format(response.data)
+            )
+
+        return json.loads(response.data.decode("utf-8"))
+
+    @property
+    def key_id(self):
+        """Optional[str]: The key ID used to identify this private key.
+
+        .. warning::
+           This is always ``None``. The key ID used by IAM can not
+           be reliably determined ahead of time.
+        """
+        return None
+
+    @_helpers.copy_docstring(crypt.Signer)
+    def sign(self, message):
+        response = self._make_signing_request(message)
+        return base64.b64decode(response["signedBlob"])
diff --git a/google/auth/identity_pool.py b/google/auth/identity_pool.py
new file mode 100644
index 0000000..fb33d77
--- /dev/null
+++ b/google/auth/identity_pool.py
@@ -0,0 +1,287 @@
+# 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.
+
+"""Identity Pool Credentials.
+
+This module provides credentials to access Google Cloud resources from on-prem
+or non-Google Cloud platforms which support external credentials (e.g. OIDC ID
+tokens) retrieved from local file locations or local servers. This includes
+Microsoft Azure and OIDC identity providers (e.g. K8s workloads registered with
+Hub with Hub workload identity enabled).
+
+These credentials are recommended over the use of service account credentials
+in on-prem/non-Google Cloud platforms as they do not involve the management of
+long-live service account private keys.
+
+Identity Pool Credentials are initialized using external_account
+arguments which are typically loaded from an external credentials file or
+an external credentials URL. Unlike other Credentials that can be initialized
+with a list of explicit arguments, secrets or credentials, external account
+clients use the environment and hints/guidelines provided by the
+external_account JSON file to retrieve credentials and exchange them for Google
+access tokens.
+"""
+
+try:
+    from collections.abc import Mapping
+# Python 2.7 compatibility
+except ImportError:  # pragma: NO COVER
+    from collections import Mapping
+import io
+import json
+import os
+
+from google.auth import _helpers
+from google.auth import exceptions
+from google.auth import external_account
+
+
+class Credentials(external_account.Credentials):
+    """External account credentials sourced from files and URLs."""
+
+    def __init__(
+        self,
+        audience,
+        subject_token_type,
+        token_url,
+        credential_source,
+        service_account_impersonation_url=None,
+        client_id=None,
+        client_secret=None,
+        quota_project_id=None,
+        scopes=None,
+        default_scopes=None,
+        workforce_pool_user_project=None,
+    ):
+        """Instantiates an external account credentials object from a file/URL.
+
+        Args:
+            audience (str): The STS audience field.
+            subject_token_type (str): The subject token type.
+            token_url (str): The STS endpoint URL.
+            credential_source (Mapping): The credential source dictionary used to
+                provide instructions on how to retrieve external credential to be
+                exchanged for Google access tokens.
+
+                Example credential_source for url-sourced credential::
+
+                    {
+                        "url": "http://www.example.com",
+                        "format": {
+                            "type": "json",
+                            "subject_token_field_name": "access_token",
+                        },
+                        "headers": {"foo": "bar"},
+                    }
+
+                Example credential_source for file-sourced credential::
+
+                    {
+                        "file": "/path/to/token/file.txt"
+                    }
+
+            service_account_impersonation_url (Optional[str]): The optional service account
+                impersonation getAccessToken URL.
+            client_id (Optional[str]): The optional client ID.
+            client_secret (Optional[str]): The optional client secret.
+            quota_project_id (Optional[str]): The optional quota project ID.
+            scopes (Optional[Sequence[str]]): Optional scopes to request during the
+                authorization grant.
+            default_scopes (Optional[Sequence[str]]): Default scopes passed by a
+                Google client library. Use 'scopes' for user-defined scopes.
+            workforce_pool_user_project (Optona[str]): The optional workforce pool user
+                project number when the credential corresponds to a workforce pool and not
+                a workload identity pool. The underlying principal must still have
+                serviceusage.services.use IAM permission to use the project for
+                billing/quota.
+
+        Raises:
+            google.auth.exceptions.RefreshError: If an error is encountered during
+                access token retrieval logic.
+            ValueError: For invalid parameters.
+
+        .. note:: Typically one of the helper constructors
+            :meth:`from_file` or
+            :meth:`from_info` are used instead of calling the constructor directly.
+        """
+
+        super(Credentials, self).__init__(
+            audience=audience,
+            subject_token_type=subject_token_type,
+            token_url=token_url,
+            credential_source=credential_source,
+            service_account_impersonation_url=service_account_impersonation_url,
+            client_id=client_id,
+            client_secret=client_secret,
+            quota_project_id=quota_project_id,
+            scopes=scopes,
+            default_scopes=default_scopes,
+            workforce_pool_user_project=workforce_pool_user_project,
+        )
+        if not isinstance(credential_source, Mapping):
+            self._credential_source_file = None
+            self._credential_source_url = None
+        else:
+            self._credential_source_file = credential_source.get("file")
+            self._credential_source_url = credential_source.get("url")
+            self._credential_source_headers = credential_source.get("headers")
+            credential_source_format = credential_source.get("format", {})
+            # Get credential_source format type. When not provided, this
+            # defaults to text.
+            self._credential_source_format_type = (
+                credential_source_format.get("type") or "text"
+            )
+            # environment_id is only supported in AWS or dedicated future external
+            # account credentials.
+            if "environment_id" in credential_source:
+                raise ValueError(
+                    "Invalid Identity Pool credential_source field 'environment_id'"
+                )
+            if self._credential_source_format_type not in ["text", "json"]:
+                raise ValueError(
+                    "Invalid credential_source format '{}'".format(
+                        self._credential_source_format_type
+                    )
+                )
+            # For JSON types, get the required subject_token field name.
+            if self._credential_source_format_type == "json":
+                self._credential_source_field_name = credential_source_format.get(
+                    "subject_token_field_name"
+                )
+                if self._credential_source_field_name is None:
+                    raise ValueError(
+                        "Missing subject_token_field_name for JSON credential_source format"
+                    )
+            else:
+                self._credential_source_field_name = None
+
+        if self._credential_source_file and self._credential_source_url:
+            raise ValueError(
+                "Ambiguous credential_source. 'file' is mutually exclusive with 'url'."
+            )
+        if not self._credential_source_file and not self._credential_source_url:
+            raise ValueError(
+                "Missing credential_source. A 'file' or 'url' must be provided."
+            )
+
+    @_helpers.copy_docstring(external_account.Credentials)
+    def retrieve_subject_token(self, request):
+        return self._parse_token_data(
+            self._get_token_data(request),
+            self._credential_source_format_type,
+            self._credential_source_field_name,
+        )
+
+    def _get_token_data(self, request):
+        if self._credential_source_file:
+            return self._get_file_data(self._credential_source_file)
+        else:
+            return self._get_url_data(
+                request, self._credential_source_url, self._credential_source_headers
+            )
+
+    def _get_file_data(self, filename):
+        if not os.path.exists(filename):
+            raise exceptions.RefreshError("File '{}' was not found.".format(filename))
+
+        with io.open(filename, "r", encoding="utf-8") as file_obj:
+            return file_obj.read(), filename
+
+    def _get_url_data(self, request, url, headers):
+        response = request(url=url, method="GET", headers=headers)
+
+        # support both string and bytes type response.data
+        response_body = (
+            response.data.decode("utf-8")
+            if hasattr(response.data, "decode")
+            else response.data
+        )
+
+        if response.status != 200:
+            raise exceptions.RefreshError(
+                "Unable to retrieve Identity Pool subject token", response_body
+            )
+
+        return response_body, url
+
+    def _parse_token_data(
+        self, token_content, format_type="text", subject_token_field_name=None
+    ):
+        content, filename = token_content
+        if format_type == "text":
+            token = content
+        else:
+            try:
+                # Parse file content as JSON.
+                response_data = json.loads(content)
+                # Get the subject_token.
+                token = response_data[subject_token_field_name]
+            except (KeyError, ValueError):
+                raise exceptions.RefreshError(
+                    "Unable to parse subject_token from JSON file '{}' using key '{}'".format(
+                        filename, subject_token_field_name
+                    )
+                )
+        if not token:
+            raise exceptions.RefreshError(
+                "Missing subject_token in the credential_source file"
+            )
+        return token
+
+    @classmethod
+    def from_info(cls, info, **kwargs):
+        """Creates an Identity Pool Credentials instance from parsed external account info.
+
+        Args:
+            info (Mapping[str, str]): The Identity Pool external account info in Google
+                format.
+            kwargs: Additional arguments to pass to the constructor.
+
+        Returns:
+            google.auth.identity_pool.Credentials: The constructed
+                credentials.
+
+        Raises:
+            ValueError: For invalid parameters.
+        """
+        return cls(
+            audience=info.get("audience"),
+            subject_token_type=info.get("subject_token_type"),
+            token_url=info.get("token_url"),
+            service_account_impersonation_url=info.get(
+                "service_account_impersonation_url"
+            ),
+            client_id=info.get("client_id"),
+            client_secret=info.get("client_secret"),
+            credential_source=info.get("credential_source"),
+            quota_project_id=info.get("quota_project_id"),
+            workforce_pool_user_project=info.get("workforce_pool_user_project"),
+            **kwargs
+        )
+
+    @classmethod
+    def from_file(cls, filename, **kwargs):
+        """Creates an IdentityPool Credentials instance from an external account json file.
+
+        Args:
+            filename (str): The path to the IdentityPool external account json file.
+            kwargs: Additional arguments to pass to the constructor.
+
+        Returns:
+            google.auth.identity_pool.Credentials: The constructed
+                credentials.
+        """
+        with io.open(filename, "r", encoding="utf-8") as json_file:
+            data = json.load(json_file)
+            return cls.from_info(data, **kwargs)
diff --git a/google/auth/impersonated_credentials.py b/google/auth/impersonated_credentials.py
new file mode 100644
index 0000000..80d6fdf
--- /dev/null
+++ b/google/auth/impersonated_credentials.py
@@ -0,0 +1,417 @@
+# Copyright 2018 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Google Cloud Impersonated credentials.
+
+This module provides authentication for applications where local credentials
+impersonates a remote service account using `IAM Credentials API`_.
+
+This class can be used to impersonate a service account as long as the original
+Credential object has the "Service Account Token Creator" role on the target
+service account.
+
+    .. _IAM Credentials API:
+        https://cloud.google.com/iam/credentials/reference/rest/
+"""
+
+import base64
+import copy
+from datetime import datetime
+import json
+
+import six
+from six.moves import http_client
+
+from google.auth import _helpers
+from google.auth import credentials
+from google.auth import exceptions
+from google.auth import jwt
+from google.auth.transport.requests import AuthorizedSession
+
+_DEFAULT_TOKEN_LIFETIME_SECS = 3600  # 1 hour in seconds
+
+_IAM_SCOPE = ["https://www.googleapis.com/auth/iam"]
+
+_IAM_ENDPOINT = (
+    "https://iamcredentials.googleapis.com/v1/projects/-"
+    + "/serviceAccounts/{}:generateAccessToken"
+)
+
+_IAM_SIGN_ENDPOINT = (
+    "https://iamcredentials.googleapis.com/v1/projects/-"
+    + "/serviceAccounts/{}:signBlob"
+)
+
+_IAM_IDTOKEN_ENDPOINT = (
+    "https://iamcredentials.googleapis.com/v1/"
+    + "projects/-/serviceAccounts/{}:generateIdToken"
+)
+
+_REFRESH_ERROR = "Unable to acquire impersonated credentials"
+
+_DEFAULT_TOKEN_LIFETIME_SECS = 3600  # 1 hour in seconds
+
+_DEFAULT_TOKEN_URI = "https://oauth2.googleapis.com/token"
+
+
+def _make_iam_token_request(
+    request, principal, headers, body, iam_endpoint_override=None
+):
+    """Makes a request to the Google Cloud IAM service for an access token.
+    Args:
+        request (Request): The Request object to use.
+        principal (str): The principal to request an access token for.
+        headers (Mapping[str, str]): Map of headers to transmit.
+        body (Mapping[str, str]): JSON Payload body for the iamcredentials
+            API call.
+        iam_endpoint_override (Optiona[str]): The full IAM endpoint override
+            with the target_principal embedded. This is useful when supporting
+            impersonation with regional endpoints.
+
+    Raises:
+        google.auth.exceptions.TransportError: Raised if there is an underlying
+            HTTP connection error
+        google.auth.exceptions.RefreshError: Raised if the impersonated
+            credentials are not available.  Common reasons are
+            `iamcredentials.googleapis.com` is not enabled or the
+            `Service Account Token Creator` is not assigned
+    """
+    iam_endpoint = iam_endpoint_override or _IAM_ENDPOINT.format(principal)
+
+    body = json.dumps(body).encode("utf-8")
+
+    response = request(url=iam_endpoint, method="POST", headers=headers, body=body)
+
+    # support both string and bytes type response.data
+    response_body = (
+        response.data.decode("utf-8")
+        if hasattr(response.data, "decode")
+        else response.data
+    )
+
+    if response.status != http_client.OK:
+        exceptions.RefreshError(_REFRESH_ERROR, response_body)
+
+    try:
+        token_response = json.loads(response_body)
+        token = token_response["accessToken"]
+        expiry = datetime.strptime(token_response["expireTime"], "%Y-%m-%dT%H:%M:%SZ")
+
+        return token, expiry
+
+    except (KeyError, ValueError) as caught_exc:
+        new_exc = exceptions.RefreshError(
+            "{}: No access token or invalid expiration in response.".format(
+                _REFRESH_ERROR
+            ),
+            response_body,
+        )
+        six.raise_from(new_exc, caught_exc)
+
+
+class Credentials(credentials.CredentialsWithQuotaProject, credentials.Signing):
+    """This module defines impersonated credentials which are essentially
+    impersonated identities.
+
+    Impersonated Credentials allows credentials issued to a user or
+    service account to impersonate another. The target service account must
+    grant the originating credential principal the
+    `Service Account Token Creator`_ IAM role:
+
+    For more information about Token Creator IAM role and
+    IAMCredentials API, see
+    `Creating Short-Lived Service Account Credentials`_.
+
+    .. _Service Account Token Creator:
+        https://cloud.google.com/iam/docs/service-accounts#the_service_account_token_creator_role
+
+    .. _Creating Short-Lived Service Account Credentials:
+        https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials
+
+    Usage:
+
+    First grant source_credentials the `Service Account Token Creator`
+    role on the target account to impersonate.   In this example, the
+    service account represented by svc_account.json has the
+    token creator role on
+    `impersonated-account@_project_.iam.gserviceaccount.com`.
+
+    Enable the IAMCredentials API on the source project:
+    `gcloud services enable iamcredentials.googleapis.com`.
+
+    Initialize a source credential which does not have access to
+    list bucket::
+
+        from google.oauth2 import service_account
+
+        target_scopes = [
+            'https://www.googleapis.com/auth/devstorage.read_only']
+
+        source_credentials = (
+            service_account.Credentials.from_service_account_file(
+                '/path/to/svc_account.json',
+                scopes=target_scopes))
+
+    Now use the source credentials to acquire credentials to impersonate
+    another service account::
+
+        from google.auth import impersonated_credentials
+
+        target_credentials = impersonated_credentials.Credentials(
+          source_credentials=source_credentials,
+          target_principal='impersonated-account@_project_.iam.gserviceaccount.com',
+          target_scopes = target_scopes,
+          lifetime=500)
+
+    Resource access is granted::
+
+        client = storage.Client(credentials=target_credentials)
+        buckets = client.list_buckets(project='your_project')
+        for bucket in buckets:
+          print(bucket.name)
+    """
+
+    def __init__(
+        self,
+        source_credentials,
+        target_principal,
+        target_scopes,
+        delegates=None,
+        lifetime=_DEFAULT_TOKEN_LIFETIME_SECS,
+        quota_project_id=None,
+        iam_endpoint_override=None,
+    ):
+        """
+        Args:
+            source_credentials (google.auth.Credentials): The source credential
+                used as to acquire the impersonated credentials.
+            target_principal (str): The service account to impersonate.
+            target_scopes (Sequence[str]): Scopes to request during the
+                authorization grant.
+            delegates (Sequence[str]): The chained list of delegates required
+                to grant the final access_token.  If set, the sequence of
+                identities must have "Service Account Token Creator" capability
+                granted to the prceeding identity.  For example, if set to
+                [serviceAccountB, serviceAccountC], the source_credential
+                must have the Token Creator role on serviceAccountB.
+                serviceAccountB must have the Token Creator on
+                serviceAccountC.
+                Finally, C must have Token Creator on target_principal.
+                If left unset, source_credential must have that role on
+                target_principal.
+            lifetime (int): Number of seconds the delegated credential should
+                be valid for (upto 3600).
+            quota_project_id (Optional[str]): The project ID used for quota and billing.
+                This project may be different from the project used to
+                create the credentials.
+            iam_endpoint_override (Optiona[str]): The full IAM endpoint override
+                with the target_principal embedded. This is useful when supporting
+                impersonation with regional endpoints.
+        """
+
+        super(Credentials, self).__init__()
+
+        self._source_credentials = copy.copy(source_credentials)
+        # Service account source credentials must have the _IAM_SCOPE
+        # added to refresh correctly. User credentials cannot have
+        # their original scopes modified.
+        if isinstance(self._source_credentials, credentials.Scoped):
+            self._source_credentials = self._source_credentials.with_scopes(_IAM_SCOPE)
+        self._target_principal = target_principal
+        self._target_scopes = target_scopes
+        self._delegates = delegates
+        self._lifetime = lifetime
+        self.token = None
+        self.expiry = _helpers.utcnow()
+        self._quota_project_id = quota_project_id
+        self._iam_endpoint_override = iam_endpoint_override
+
+    @_helpers.copy_docstring(credentials.Credentials)
+    def refresh(self, request):
+        self._update_token(request)
+
+    def _update_token(self, request):
+        """Updates credentials with a new access_token representing
+        the impersonated account.
+
+        Args:
+            request (google.auth.transport.requests.Request): Request object
+                to use for refreshing credentials.
+        """
+
+        # Refresh our source credentials if it is not valid.
+        if not self._source_credentials.valid:
+            self._source_credentials.refresh(request)
+
+        body = {
+            "delegates": self._delegates,
+            "scope": self._target_scopes,
+            "lifetime": str(self._lifetime) + "s",
+        }
+
+        headers = {"Content-Type": "application/json"}
+
+        # Apply the source credentials authentication info.
+        self._source_credentials.apply(headers)
+
+        self.token, self.expiry = _make_iam_token_request(
+            request=request,
+            principal=self._target_principal,
+            headers=headers,
+            body=body,
+            iam_endpoint_override=self._iam_endpoint_override,
+        )
+
+    def sign_bytes(self, message):
+
+        iam_sign_endpoint = _IAM_SIGN_ENDPOINT.format(self._target_principal)
+
+        body = {
+            "payload": base64.b64encode(message).decode("utf-8"),
+            "delegates": self._delegates,
+        }
+
+        headers = {"Content-Type": "application/json"}
+
+        authed_session = AuthorizedSession(self._source_credentials)
+
+        response = authed_session.post(
+            url=iam_sign_endpoint, headers=headers, json=body
+        )
+
+        if response.status_code != http_client.OK:
+            raise exceptions.TransportError(
+                "Error calling sign_bytes: {}".format(response.json())
+            )
+
+        return base64.b64decode(response.json()["signedBlob"])
+
+    @property
+    def signer_email(self):
+        return self._target_principal
+
+    @property
+    def service_account_email(self):
+        return self._target_principal
+
+    @property
+    def signer(self):
+        return self
+
+    @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
+    def with_quota_project(self, quota_project_id):
+        return self.__class__(
+            self._source_credentials,
+            target_principal=self._target_principal,
+            target_scopes=self._target_scopes,
+            delegates=self._delegates,
+            lifetime=self._lifetime,
+            quota_project_id=quota_project_id,
+            iam_endpoint_override=self._iam_endpoint_override,
+        )
+
+
+class IDTokenCredentials(credentials.CredentialsWithQuotaProject):
+    """Open ID Connect ID Token-based service account credentials.
+
+    """
+
+    def __init__(
+        self,
+        target_credentials,
+        target_audience=None,
+        include_email=False,
+        quota_project_id=None,
+    ):
+        """
+        Args:
+            target_credentials (google.auth.Credentials): The target
+                credential used as to acquire the id tokens for.
+            target_audience (string): Audience to issue the token for.
+            include_email (bool): Include email in IdToken
+            quota_project_id (Optional[str]):  The project ID used for
+                quota and billing.
+        """
+        super(IDTokenCredentials, self).__init__()
+
+        if not isinstance(target_credentials, Credentials):
+            raise exceptions.GoogleAuthError(
+                "Provided Credential must be " "impersonated_credentials"
+            )
+        self._target_credentials = target_credentials
+        self._target_audience = target_audience
+        self._include_email = include_email
+        self._quota_project_id = quota_project_id
+
+    def from_credentials(self, target_credentials, target_audience=None):
+        return self.__class__(
+            target_credentials=self._target_credentials,
+            target_audience=target_audience,
+            include_email=self._include_email,
+            quota_project_id=self._quota_project_id,
+        )
+
+    def with_target_audience(self, target_audience):
+        return self.__class__(
+            target_credentials=self._target_credentials,
+            target_audience=target_audience,
+            include_email=self._include_email,
+            quota_project_id=self._quota_project_id,
+        )
+
+    def with_include_email(self, include_email):
+        return self.__class__(
+            target_credentials=self._target_credentials,
+            target_audience=self._target_audience,
+            include_email=include_email,
+            quota_project_id=self._quota_project_id,
+        )
+
+    @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
+    def with_quota_project(self, quota_project_id):
+        return self.__class__(
+            target_credentials=self._target_credentials,
+            target_audience=self._target_audience,
+            include_email=self._include_email,
+            quota_project_id=quota_project_id,
+        )
+
+    @_helpers.copy_docstring(credentials.Credentials)
+    def refresh(self, request):
+
+        iam_sign_endpoint = _IAM_IDTOKEN_ENDPOINT.format(
+            self._target_credentials.signer_email
+        )
+
+        body = {
+            "audience": self._target_audience,
+            "delegates": self._target_credentials._delegates,
+            "includeEmail": self._include_email,
+        }
+
+        headers = {"Content-Type": "application/json"}
+
+        authed_session = AuthorizedSession(
+            self._target_credentials._source_credentials, auth_request=request
+        )
+
+        response = authed_session.post(
+            url=iam_sign_endpoint,
+            headers=headers,
+            data=json.dumps(body).encode("utf-8"),
+        )
+
+        id_token = response.json()["token"]
+        self.token = id_token
+        self.expiry = datetime.fromtimestamp(jwt.decode(id_token, verify=False)["exp"])
diff --git a/google/auth/jwt.py b/google/auth/jwt.py
new file mode 100644
index 0000000..d565595
--- /dev/null
+++ b/google/auth/jwt.py
@@ -0,0 +1,857 @@
+# Copyright 2016 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.
+
+"""JSON Web Tokens
+
+Provides support for creating (encoding) and verifying (decoding) JWTs,
+especially JWTs generated and consumed by Google infrastructure.
+
+See `rfc7519`_ for more details on JWTs.
+
+To encode a JWT use :func:`encode`::
+
+    from google.auth import crypt
+    from google.auth import jwt
+
+    signer = crypt.Signer(private_key)
+    payload = {'some': 'payload'}
+    encoded = jwt.encode(signer, payload)
+
+To decode a JWT and verify claims use :func:`decode`::
+
+    claims = jwt.decode(encoded, certs=public_certs)
+
+You can also skip verification::
+
+    claims = jwt.decode(encoded, verify=False)
+
+.. _rfc7519: https://tools.ietf.org/html/rfc7519
+
+"""
+
+try:
+    from collections.abc import Mapping
+# Python 2.7 compatibility
+except ImportError:  # pragma: NO COVER
+    from collections import Mapping
+import copy
+import datetime
+import json
+
+import cachetools
+import six
+from six.moves import urllib
+
+from google.auth import _helpers
+from google.auth import _service_account_info
+from google.auth import crypt
+from google.auth import exceptions
+import google.auth.credentials
+
+try:
+    from google.auth.crypt import es256
+except ImportError:  # pragma: NO COVER
+    es256 = None
+
+_DEFAULT_TOKEN_LIFETIME_SECS = 3600  # 1 hour in seconds
+_DEFAULT_MAX_CACHE_SIZE = 10
+_ALGORITHM_TO_VERIFIER_CLASS = {"RS256": crypt.RSAVerifier}
+_CRYPTOGRAPHY_BASED_ALGORITHMS = frozenset(["ES256"])
+
+if es256 is not None:  # pragma: NO COVER
+    _ALGORITHM_TO_VERIFIER_CLASS["ES256"] = es256.ES256Verifier
+
+
+def encode(signer, payload, header=None, key_id=None):
+    """Make a signed JWT.
+
+    Args:
+        signer (google.auth.crypt.Signer): The signer used to sign the JWT.
+        payload (Mapping[str, str]): The JWT payload.
+        header (Mapping[str, str]): Additional JWT header payload.
+        key_id (str): The key id to add to the JWT header. If the
+            signer has a key id it will be used as the default. If this is
+            specified it will override the signer's key id.
+
+    Returns:
+        bytes: The encoded JWT.
+    """
+    if header is None:
+        header = {}
+
+    if key_id is None:
+        key_id = signer.key_id
+
+    header.update({"typ": "JWT"})
+
+    if "alg" not in header:
+        if es256 is not None and isinstance(signer, es256.ES256Signer):
+            header.update({"alg": "ES256"})
+        else:
+            header.update({"alg": "RS256"})
+
+    if key_id is not None:
+        header["kid"] = key_id
+
+    segments = [
+        _helpers.unpadded_urlsafe_b64encode(json.dumps(header).encode("utf-8")),
+        _helpers.unpadded_urlsafe_b64encode(json.dumps(payload).encode("utf-8")),
+    ]
+
+    signing_input = b".".join(segments)
+    signature = signer.sign(signing_input)
+    segments.append(_helpers.unpadded_urlsafe_b64encode(signature))
+
+    return b".".join(segments)
+
+
+def _decode_jwt_segment(encoded_section):
+    """Decodes a single JWT segment."""
+    section_bytes = _helpers.padded_urlsafe_b64decode(encoded_section)
+    try:
+        return json.loads(section_bytes.decode("utf-8"))
+    except ValueError as caught_exc:
+        new_exc = ValueError("Can't parse segment: {0}".format(section_bytes))
+        six.raise_from(new_exc, caught_exc)
+
+
+def _unverified_decode(token):
+    """Decodes a token and does no verification.
+
+    Args:
+        token (Union[str, bytes]): The encoded JWT.
+
+    Returns:
+        Tuple[str, str, str, str]: header, payload, signed_section, and
+            signature.
+
+    Raises:
+        ValueError: if there are an incorrect amount of segments in the token.
+    """
+    token = _helpers.to_bytes(token)
+
+    if token.count(b".") != 2:
+        raise ValueError("Wrong number of segments in token: {0}".format(token))
+
+    encoded_header, encoded_payload, signature = token.split(b".")
+    signed_section = encoded_header + b"." + encoded_payload
+    signature = _helpers.padded_urlsafe_b64decode(signature)
+
+    # Parse segments
+    header = _decode_jwt_segment(encoded_header)
+    payload = _decode_jwt_segment(encoded_payload)
+
+    return header, payload, signed_section, signature
+
+
+def decode_header(token):
+    """Return the decoded header of a token.
+
+    No verification is done. This is useful to extract the key id from
+    the header in order to acquire the appropriate certificate to verify
+    the token.
+
+    Args:
+        token (Union[str, bytes]): the encoded JWT.
+
+    Returns:
+        Mapping: The decoded JWT header.
+    """
+    header, _, _, _ = _unverified_decode(token)
+    return header
+
+
+def _verify_iat_and_exp(payload, clock_skew_in_seconds=0):
+    """Verifies the ``iat`` (Issued At) and ``exp`` (Expires) claims in a token
+    payload.
+
+    Args:
+        payload (Mapping[str, str]): The JWT payload.
+        clock_skew_in_seconds (int): The clock skew used for `iat` and `exp`
+            validation.
+
+    Raises:
+        ValueError: if any checks failed.
+    """
+    now = _helpers.datetime_to_secs(_helpers.utcnow())
+
+    # Make sure the iat and exp claims are present.
+    for key in ("iat", "exp"):
+        if key not in payload:
+            raise ValueError("Token does not contain required claim {}".format(key))
+
+    # Make sure the token wasn't issued in the future.
+    iat = payload["iat"]
+    # Err on the side of accepting a token that is slightly early to account
+    # for clock skew.
+    earliest = iat - clock_skew_in_seconds
+    if now < earliest:
+        raise ValueError(
+            "Token used too early, {} < {}. Check that your computer's clock is set correctly.".format(
+                now, iat
+            )
+        )
+
+    # Make sure the token wasn't issued in the past.
+    exp = payload["exp"]
+    # Err on the side of accepting a token that is slightly out of date
+    # to account for clow skew.
+    latest = exp + clock_skew_in_seconds
+    if latest < now:
+        raise ValueError("Token expired, {} < {}".format(latest, now))
+
+
+def decode(token, certs=None, verify=True, audience=None, clock_skew_in_seconds=0):
+    """Decode and verify a JWT.
+
+    Args:
+        token (str): The encoded JWT.
+        certs (Union[str, bytes, Mapping[str, Union[str, bytes]]]): The
+            certificate used to validate the JWT signature. If bytes or string,
+            it must the the public key certificate in PEM format. If a mapping,
+            it must be a mapping of key IDs to public key certificates in PEM
+            format. The mapping must contain the same key ID that's specified
+            in the token's header.
+        verify (bool): Whether to perform signature and claim validation.
+            Verification is done by default.
+        audience (str or list): The audience claim, 'aud', that this JWT should
+            contain. Or a list of audience claims. If None then the JWT's 'aud'
+            parameter is not verified.
+        clock_skew_in_seconds (int): The clock skew used for `iat` and `exp`
+            validation.
+
+    Returns:
+        Mapping[str, str]: The deserialized JSON payload in the JWT.
+
+    Raises:
+        ValueError: if any verification checks failed.
+    """
+    header, payload, signed_section, signature = _unverified_decode(token)
+
+    if not verify:
+        return payload
+
+    # Pluck the key id and algorithm from the header and make sure we have
+    # a verifier that can support it.
+    key_alg = header.get("alg")
+    key_id = header.get("kid")
+
+    try:
+        verifier_cls = _ALGORITHM_TO_VERIFIER_CLASS[key_alg]
+    except KeyError as exc:
+        if key_alg in _CRYPTOGRAPHY_BASED_ALGORITHMS:
+            six.raise_from(
+                ValueError(
+                    "The key algorithm {} requires the cryptography package "
+                    "to be installed.".format(key_alg)
+                ),
+                exc,
+            )
+        else:
+            six.raise_from(
+                ValueError("Unsupported signature algorithm {}".format(key_alg)), exc
+            )
+
+    # If certs is specified as a dictionary of key IDs to certificates, then
+    # use the certificate identified by the key ID in the token header.
+    if isinstance(certs, Mapping):
+        if key_id:
+            if key_id not in certs:
+                raise ValueError("Certificate for key id {} not found.".format(key_id))
+            certs_to_check = [certs[key_id]]
+        # If there's no key id in the header, check against all of the certs.
+        else:
+            certs_to_check = certs.values()
+    else:
+        certs_to_check = certs
+
+    # Verify that the signature matches the message.
+    if not crypt.verify_signature(
+        signed_section, signature, certs_to_check, verifier_cls
+    ):
+        raise ValueError("Could not verify token signature.")
+
+    # Verify the issued at and created times in the payload.
+    _verify_iat_and_exp(payload, clock_skew_in_seconds)
+
+    # Check audience.
+    if audience is not None:
+        claim_audience = payload.get("aud")
+        if isinstance(audience, str):
+            audience = [audience]
+        if claim_audience not in audience:
+            raise ValueError(
+                "Token has wrong audience {}, expected one of {}".format(
+                    claim_audience, audience
+                )
+            )
+
+    return payload
+
+
+class Credentials(
+    google.auth.credentials.Signing, google.auth.credentials.CredentialsWithQuotaProject
+):
+    """Credentials that use a JWT as the bearer token.
+
+    These credentials require an "audience" claim. This claim identifies the
+    intended recipient of the bearer token.
+
+    The constructor arguments determine the claims for the JWT that is
+    sent with requests. Usually, you'll construct these credentials with
+    one of the helper constructors as shown in the next section.
+
+    To create JWT credentials using a Google service account private key
+    JSON file::
+
+        audience = 'https://pubsub.googleapis.com/google.pubsub.v1.Publisher'
+        credentials = jwt.Credentials.from_service_account_file(
+            'service-account.json',
+            audience=audience)
+
+    If you already have the service account file loaded and parsed::
+
+        service_account_info = json.load(open('service_account.json'))
+        credentials = jwt.Credentials.from_service_account_info(
+            service_account_info,
+            audience=audience)
+
+    Both helper methods pass on arguments to the constructor, so you can
+    specify the JWT claims::
+
+        credentials = jwt.Credentials.from_service_account_file(
+            'service-account.json',
+            audience=audience,
+            additional_claims={'meta': 'data'})
+
+    You can also construct the credentials directly if you have a
+    :class:`~google.auth.crypt.Signer` instance::
+
+        credentials = jwt.Credentials(
+            signer,
+            issuer='your-issuer',
+            subject='your-subject',
+            audience=audience)
+
+    The claims are considered immutable. If you want to modify the claims,
+    you can easily create another instance using :meth:`with_claims`::
+
+        new_audience = (
+            'https://pubsub.googleapis.com/google.pubsub.v1.Subscriber')
+        new_credentials = credentials.with_claims(audience=new_audience)
+    """
+
+    def __init__(
+        self,
+        signer,
+        issuer,
+        subject,
+        audience,
+        additional_claims=None,
+        token_lifetime=_DEFAULT_TOKEN_LIFETIME_SECS,
+        quota_project_id=None,
+    ):
+        """
+        Args:
+            signer (google.auth.crypt.Signer): The signer used to sign JWTs.
+            issuer (str): The `iss` claim.
+            subject (str): The `sub` claim.
+            audience (str): the `aud` claim. The intended audience for the
+                credentials.
+            additional_claims (Mapping[str, str]): Any additional claims for
+                the JWT payload.
+            token_lifetime (int): The amount of time in seconds for
+                which the token is valid. Defaults to 1 hour.
+            quota_project_id (Optional[str]): The project ID used for quota
+                and billing.
+        """
+        super(Credentials, self).__init__()
+        self._signer = signer
+        self._issuer = issuer
+        self._subject = subject
+        self._audience = audience
+        self._token_lifetime = token_lifetime
+        self._quota_project_id = quota_project_id
+
+        if additional_claims is None:
+            additional_claims = {}
+
+        self._additional_claims = additional_claims
+
+    @classmethod
+    def _from_signer_and_info(cls, signer, info, **kwargs):
+        """Creates a Credentials instance from a signer and service account
+        info.
+
+        Args:
+            signer (google.auth.crypt.Signer): The signer used to sign JWTs.
+            info (Mapping[str, str]): The service account info.
+            kwargs: Additional arguments to pass to the constructor.
+
+        Returns:
+            google.auth.jwt.Credentials: The constructed credentials.
+
+        Raises:
+            ValueError: If the info is not in the expected format.
+        """
+        kwargs.setdefault("subject", info["client_email"])
+        kwargs.setdefault("issuer", info["client_email"])
+        return cls(signer, **kwargs)
+
+    @classmethod
+    def from_service_account_info(cls, info, **kwargs):
+        """Creates an Credentials instance from a dictionary.
+
+        Args:
+            info (Mapping[str, str]): The service account info in Google
+                format.
+            kwargs: Additional arguments to pass to the constructor.
+
+        Returns:
+            google.auth.jwt.Credentials: The constructed credentials.
+
+        Raises:
+            ValueError: If the info is not in the expected format.
+        """
+        signer = _service_account_info.from_dict(info, require=["client_email"])
+        return cls._from_signer_and_info(signer, info, **kwargs)
+
+    @classmethod
+    def from_service_account_file(cls, filename, **kwargs):
+        """Creates a Credentials instance from a service account .json file
+        in Google format.
+
+        Args:
+            filename (str): The path to the service account .json file.
+            kwargs: Additional arguments to pass to the constructor.
+
+        Returns:
+            google.auth.jwt.Credentials: The constructed credentials.
+        """
+        info, signer = _service_account_info.from_filename(
+            filename, require=["client_email"]
+        )
+        return cls._from_signer_and_info(signer, info, **kwargs)
+
+    @classmethod
+    def from_signing_credentials(cls, credentials, audience, **kwargs):
+        """Creates a new :class:`google.auth.jwt.Credentials` instance from an
+        existing :class:`google.auth.credentials.Signing` instance.
+
+        The new instance will use the same signer as the existing instance and
+        will use the existing instance's signer email as the issuer and
+        subject by default.
+
+        Example::
+
+            svc_creds = service_account.Credentials.from_service_account_file(
+                'service_account.json')
+            audience = (
+                'https://pubsub.googleapis.com/google.pubsub.v1.Publisher')
+            jwt_creds = jwt.Credentials.from_signing_credentials(
+                svc_creds, audience=audience)
+
+        Args:
+            credentials (google.auth.credentials.Signing): The credentials to
+                use to construct the new credentials.
+            audience (str): the `aud` claim. The intended audience for the
+                credentials.
+            kwargs: Additional arguments to pass to the constructor.
+
+        Returns:
+            google.auth.jwt.Credentials: A new Credentials instance.
+        """
+        kwargs.setdefault("issuer", credentials.signer_email)
+        kwargs.setdefault("subject", credentials.signer_email)
+        return cls(credentials.signer, audience=audience, **kwargs)
+
+    def with_claims(
+        self, issuer=None, subject=None, audience=None, additional_claims=None
+    ):
+        """Returns a copy of these credentials with modified claims.
+
+        Args:
+            issuer (str): The `iss` claim. If unspecified the current issuer
+                claim will be used.
+            subject (str): The `sub` claim. If unspecified the current subject
+                claim will be used.
+            audience (str): the `aud` claim. If unspecified the current
+                audience claim will be used.
+            additional_claims (Mapping[str, str]): Any additional claims for
+                the JWT payload. This will be merged with the current
+                additional claims.
+
+        Returns:
+            google.auth.jwt.Credentials: A new credentials instance.
+        """
+        new_additional_claims = copy.deepcopy(self._additional_claims)
+        new_additional_claims.update(additional_claims or {})
+
+        return self.__class__(
+            self._signer,
+            issuer=issuer if issuer is not None else self._issuer,
+            subject=subject if subject is not None else self._subject,
+            audience=audience if audience is not None else self._audience,
+            additional_claims=new_additional_claims,
+            quota_project_id=self._quota_project_id,
+        )
+
+    @_helpers.copy_docstring(google.auth.credentials.CredentialsWithQuotaProject)
+    def with_quota_project(self, quota_project_id):
+        return self.__class__(
+            self._signer,
+            issuer=self._issuer,
+            subject=self._subject,
+            audience=self._audience,
+            additional_claims=self._additional_claims,
+            quota_project_id=quota_project_id,
+        )
+
+    def _make_jwt(self):
+        """Make a signed JWT.
+
+        Returns:
+            Tuple[bytes, datetime]: The encoded JWT and the expiration.
+        """
+        now = _helpers.utcnow()
+        lifetime = datetime.timedelta(seconds=self._token_lifetime)
+        expiry = now + lifetime
+
+        payload = {
+            "iss": self._issuer,
+            "sub": self._subject,
+            "iat": _helpers.datetime_to_secs(now),
+            "exp": _helpers.datetime_to_secs(expiry),
+        }
+        if self._audience:
+            payload["aud"] = self._audience
+
+        payload.update(self._additional_claims)
+
+        jwt = encode(self._signer, payload)
+
+        return jwt, expiry
+
+    def refresh(self, request):
+        """Refreshes the access token.
+
+        Args:
+            request (Any): Unused.
+        """
+        # pylint: disable=unused-argument
+        # (pylint doesn't correctly recognize overridden methods.)
+        self.token, self.expiry = self._make_jwt()
+
+    @_helpers.copy_docstring(google.auth.credentials.Signing)
+    def sign_bytes(self, message):
+        return self._signer.sign(message)
+
+    @property
+    @_helpers.copy_docstring(google.auth.credentials.Signing)
+    def signer_email(self):
+        return self._issuer
+
+    @property
+    @_helpers.copy_docstring(google.auth.credentials.Signing)
+    def signer(self):
+        return self._signer
+
+
+class OnDemandCredentials(
+    google.auth.credentials.Signing, google.auth.credentials.CredentialsWithQuotaProject
+):
+    """On-demand JWT credentials.
+
+    Like :class:`Credentials`, this class uses a JWT as the bearer token for
+    authentication. However, this class does not require the audience at
+    construction time. Instead, it will generate a new token on-demand for
+    each request using the request URI as the audience. It caches tokens
+    so that multiple requests to the same URI do not incur the overhead
+    of generating a new token every time.
+
+    This behavior is especially useful for `gRPC`_ clients. A gRPC service may
+    have multiple audience and gRPC clients may not know all of the audiences
+    required for accessing a particular service. With these credentials,
+    no knowledge of the audiences is required ahead of time.
+
+    .. _grpc: http://www.grpc.io/
+    """
+
+    def __init__(
+        self,
+        signer,
+        issuer,
+        subject,
+        additional_claims=None,
+        token_lifetime=_DEFAULT_TOKEN_LIFETIME_SECS,
+        max_cache_size=_DEFAULT_MAX_CACHE_SIZE,
+        quota_project_id=None,
+    ):
+        """
+        Args:
+            signer (google.auth.crypt.Signer): The signer used to sign JWTs.
+            issuer (str): The `iss` claim.
+            subject (str): The `sub` claim.
+            additional_claims (Mapping[str, str]): Any additional claims for
+                the JWT payload.
+            token_lifetime (int): The amount of time in seconds for
+                which the token is valid. Defaults to 1 hour.
+            max_cache_size (int): The maximum number of JWT tokens to keep in
+                cache. Tokens are cached using :class:`cachetools.LRUCache`.
+            quota_project_id (Optional[str]): The project ID used for quota
+                and billing.
+
+        """
+        super(OnDemandCredentials, self).__init__()
+        self._signer = signer
+        self._issuer = issuer
+        self._subject = subject
+        self._token_lifetime = token_lifetime
+        self._quota_project_id = quota_project_id
+
+        if additional_claims is None:
+            additional_claims = {}
+
+        self._additional_claims = additional_claims
+        self._cache = cachetools.LRUCache(maxsize=max_cache_size)
+
+    @classmethod
+    def _from_signer_and_info(cls, signer, info, **kwargs):
+        """Creates an OnDemandCredentials instance from a signer and service
+        account info.
+
+        Args:
+            signer (google.auth.crypt.Signer): The signer used to sign JWTs.
+            info (Mapping[str, str]): The service account info.
+            kwargs: Additional arguments to pass to the constructor.
+
+        Returns:
+            google.auth.jwt.OnDemandCredentials: The constructed credentials.
+
+        Raises:
+            ValueError: If the info is not in the expected format.
+        """
+        kwargs.setdefault("subject", info["client_email"])
+        kwargs.setdefault("issuer", info["client_email"])
+        return cls(signer, **kwargs)
+
+    @classmethod
+    def from_service_account_info(cls, info, **kwargs):
+        """Creates an OnDemandCredentials instance from a dictionary.
+
+        Args:
+            info (Mapping[str, str]): The service account info in Google
+                format.
+            kwargs: Additional arguments to pass to the constructor.
+
+        Returns:
+            google.auth.jwt.OnDemandCredentials: The constructed credentials.
+
+        Raises:
+            ValueError: If the info is not in the expected format.
+        """
+        signer = _service_account_info.from_dict(info, require=["client_email"])
+        return cls._from_signer_and_info(signer, info, **kwargs)
+
+    @classmethod
+    def from_service_account_file(cls, filename, **kwargs):
+        """Creates an OnDemandCredentials instance from a service account .json
+        file in Google format.
+
+        Args:
+            filename (str): The path to the service account .json file.
+            kwargs: Additional arguments to pass to the constructor.
+
+        Returns:
+            google.auth.jwt.OnDemandCredentials: The constructed credentials.
+        """
+        info, signer = _service_account_info.from_filename(
+            filename, require=["client_email"]
+        )
+        return cls._from_signer_and_info(signer, info, **kwargs)
+
+    @classmethod
+    def from_signing_credentials(cls, credentials, **kwargs):
+        """Creates a new :class:`google.auth.jwt.OnDemandCredentials` instance
+        from an existing :class:`google.auth.credentials.Signing` instance.
+
+        The new instance will use the same signer as the existing instance and
+        will use the existing instance's signer email as the issuer and
+        subject by default.
+
+        Example::
+
+            svc_creds = service_account.Credentials.from_service_account_file(
+                'service_account.json')
+            jwt_creds = jwt.OnDemandCredentials.from_signing_credentials(
+                svc_creds)
+
+        Args:
+            credentials (google.auth.credentials.Signing): The credentials to
+                use to construct the new credentials.
+            kwargs: Additional arguments to pass to the constructor.
+
+        Returns:
+            google.auth.jwt.Credentials: A new Credentials instance.
+        """
+        kwargs.setdefault("issuer", credentials.signer_email)
+        kwargs.setdefault("subject", credentials.signer_email)
+        return cls(credentials.signer, **kwargs)
+
+    def with_claims(self, issuer=None, subject=None, additional_claims=None):
+        """Returns a copy of these credentials with modified claims.
+
+        Args:
+            issuer (str): The `iss` claim. If unspecified the current issuer
+                claim will be used.
+            subject (str): The `sub` claim. If unspecified the current subject
+                claim will be used.
+            additional_claims (Mapping[str, str]): Any additional claims for
+                the JWT payload. This will be merged with the current
+                additional claims.
+
+        Returns:
+            google.auth.jwt.OnDemandCredentials: A new credentials instance.
+        """
+        new_additional_claims = copy.deepcopy(self._additional_claims)
+        new_additional_claims.update(additional_claims or {})
+
+        return self.__class__(
+            self._signer,
+            issuer=issuer if issuer is not None else self._issuer,
+            subject=subject if subject is not None else self._subject,
+            additional_claims=new_additional_claims,
+            max_cache_size=self._cache.maxsize,
+            quota_project_id=self._quota_project_id,
+        )
+
+    @_helpers.copy_docstring(google.auth.credentials.CredentialsWithQuotaProject)
+    def with_quota_project(self, quota_project_id):
+
+        return self.__class__(
+            self._signer,
+            issuer=self._issuer,
+            subject=self._subject,
+            additional_claims=self._additional_claims,
+            max_cache_size=self._cache.maxsize,
+            quota_project_id=quota_project_id,
+        )
+
+    @property
+    def valid(self):
+        """Checks the validity of the credentials.
+
+        These credentials are always valid because it generates tokens on
+        demand.
+        """
+        return True
+
+    def _make_jwt_for_audience(self, audience):
+        """Make a new JWT for the given audience.
+
+        Args:
+            audience (str): The intended audience.
+
+        Returns:
+            Tuple[bytes, datetime]: The encoded JWT and the expiration.
+        """
+        now = _helpers.utcnow()
+        lifetime = datetime.timedelta(seconds=self._token_lifetime)
+        expiry = now + lifetime
+
+        payload = {
+            "iss": self._issuer,
+            "sub": self._subject,
+            "iat": _helpers.datetime_to_secs(now),
+            "exp": _helpers.datetime_to_secs(expiry),
+            "aud": audience,
+        }
+
+        payload.update(self._additional_claims)
+
+        jwt = encode(self._signer, payload)
+
+        return jwt, expiry
+
+    def _get_jwt_for_audience(self, audience):
+        """Get a JWT For a given audience.
+
+        If there is already an existing, non-expired token in the cache for
+        the audience, that token is used. Otherwise, a new token will be
+        created.
+
+        Args:
+            audience (str): The intended audience.
+
+        Returns:
+            bytes: The encoded JWT.
+        """
+        token, expiry = self._cache.get(audience, (None, None))
+
+        if token is None or expiry < _helpers.utcnow():
+            token, expiry = self._make_jwt_for_audience(audience)
+            self._cache[audience] = token, expiry
+
+        return token
+
+    def refresh(self, request):
+        """Raises an exception, these credentials can not be directly
+        refreshed.
+
+        Args:
+            request (Any): Unused.
+
+        Raises:
+            google.auth.RefreshError
+        """
+        # pylint: disable=unused-argument
+        # (pylint doesn't correctly recognize overridden methods.)
+        raise exceptions.RefreshError(
+            "OnDemandCredentials can not be directly refreshed."
+        )
+
+    def before_request(self, request, method, url, headers):
+        """Performs credential-specific before request logic.
+
+        Args:
+            request (Any): Unused. JWT credentials do not need to make an
+                HTTP request to refresh.
+            method (str): The request's HTTP method.
+            url (str): The request's URI. This is used as the audience claim
+                when generating the JWT.
+            headers (Mapping): The request's headers.
+        """
+        # pylint: disable=unused-argument
+        # (pylint doesn't correctly recognize overridden methods.)
+        parts = urllib.parse.urlsplit(url)
+        # Strip query string and fragment
+        audience = urllib.parse.urlunsplit(
+            (parts.scheme, parts.netloc, parts.path, "", "")
+        )
+        token = self._get_jwt_for_audience(audience)
+        self.apply(headers, token=token)
+
+    @_helpers.copy_docstring(google.auth.credentials.Signing)
+    def sign_bytes(self, message):
+        return self._signer.sign(message)
+
+    @property
+    @_helpers.copy_docstring(google.auth.credentials.Signing)
+    def signer_email(self):
+        return self._issuer
+
+    @property
+    @_helpers.copy_docstring(google.auth.credentials.Signing)
+    def signer(self):
+        return self._signer
diff --git a/google/auth/transport/__init__.py b/google/auth/transport/__init__.py
new file mode 100644
index 0000000..374e7b4
--- /dev/null
+++ b/google/auth/transport/__init__.py
@@ -0,0 +1,97 @@
+# Copyright 2016 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.
+
+"""Transport - HTTP client library support.
+
+:mod:`google.auth` is designed to work with various HTTP client libraries such
+as urllib3 and requests. In order to work across these libraries with different
+interfaces some abstraction is needed.
+
+This module provides two interfaces that are implemented by transport adapters
+to support HTTP libraries. :class:`Request` defines the interface expected by
+:mod:`google.auth` to make requests. :class:`Response` defines the interface
+for the return value of :class:`Request`.
+"""
+
+import abc
+
+import six
+from six.moves import http_client
+
+DEFAULT_REFRESH_STATUS_CODES = (http_client.UNAUTHORIZED,)
+"""Sequence[int]:  Which HTTP status code indicate that credentials should be
+refreshed and a request should be retried.
+"""
+
+DEFAULT_MAX_REFRESH_ATTEMPTS = 2
+"""int: How many times to refresh the credentials and retry a request."""
+
+
[email protected]_metaclass(abc.ABCMeta)
+class Response(object):
+    """HTTP Response data."""
+
+    @abc.abstractproperty
+    def status(self):
+        """int: The HTTP status code."""
+        raise NotImplementedError("status must be implemented.")
+
+    @abc.abstractproperty
+    def headers(self):
+        """Mapping[str, str]: The HTTP response headers."""
+        raise NotImplementedError("headers must be implemented.")
+
+    @abc.abstractproperty
+    def data(self):
+        """bytes: The response body."""
+        raise NotImplementedError("data must be implemented.")
+
+
[email protected]_metaclass(abc.ABCMeta)
+class Request(object):
+    """Interface for a callable that makes HTTP requests.
+
+    Specific transport implementations should provide an implementation of
+    this that adapts their specific request / response API.
+
+    .. automethod:: __call__
+    """
+
+    @abc.abstractmethod
+    def __call__(
+        self, url, method="GET", body=None, headers=None, timeout=None, **kwargs
+    ):
+        """Make an HTTP request.
+
+        Args:
+            url (str): The URI to be requested.
+            method (str): The HTTP method to use for the request. Defaults
+                to 'GET'.
+            body (bytes): The payload / body in HTTP request.
+            headers (Mapping[str, str]): Request headers.
+            timeout (Optional[int]): The number of seconds to wait for a
+                response from the server. If not specified or if None, the
+                transport-specific default timeout will be used.
+            kwargs: Additionally arguments passed on to the transport's
+                request method.
+
+        Returns:
+            Response: The HTTP response.
+
+        Raises:
+            google.auth.exceptions.TransportError: If any exception occurred.
+        """
+        # pylint: disable=redundant-returns-doc, missing-raises-doc
+        # (pylint doesn't play well with abstract docstrings.)
+        raise NotImplementedError("__call__ must be implemented.")
diff --git a/google/auth/transport/_aiohttp_requests.py b/google/auth/transport/_aiohttp_requests.py
new file mode 100644
index 0000000..ab7dfef
--- /dev/null
+++ b/google/auth/transport/_aiohttp_requests.py
@@ -0,0 +1,388 @@
+# 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.
+
+"""Transport adapter for Async HTTP (aiohttp).
+
+NOTE: This async support is experimental and marked internal. This surface may
+change in minor releases.
+"""
+
+from __future__ import absolute_import
+
+import asyncio
+import functools
+
+import aiohttp
+import six
+import urllib3
+
+from google.auth import exceptions
+from google.auth import transport
+from google.auth.transport import requests
+
+# Timeout can be re-defined depending on async requirement. Currently made 60s more than
+# sync timeout.
+_DEFAULT_TIMEOUT = 180  # in seconds
+
+
+class _CombinedResponse(transport.Response):
+    """
+    In order to more closely resemble the `requests` interface, where a raw
+    and deflated content could be accessed at once, this class lazily reads the
+    stream in `transport.Response` so both return forms can be used.
+
+    The gzip and deflate transfer-encodings are automatically decoded for you
+    because the default parameter for autodecompress into the ClientSession is set
+    to False, and therefore we add this class to act as a wrapper for a user to be
+    able to access both the raw and decoded response bodies - mirroring the sync
+    implementation.
+    """
+
+    def __init__(self, response):
+        self._response = response
+        self._raw_content = None
+
+    def _is_compressed(self):
+        headers = self._response.headers
+        return "Content-Encoding" in headers and (
+            headers["Content-Encoding"] == "gzip"
+            or headers["Content-Encoding"] == "deflate"
+        )
+
+    @property
+    def status(self):
+        return self._response.status
+
+    @property
+    def headers(self):
+        return self._response.headers
+
+    @property
+    def data(self):
+        return self._response.content
+
+    async def raw_content(self):
+        if self._raw_content is None:
+            self._raw_content = await self._response.content.read()
+        return self._raw_content
+
+    async def content(self):
+        # Load raw_content if necessary
+        await self.raw_content()
+        if self._is_compressed():
+            decoder = urllib3.response.MultiDecoder(
+                self._response.headers["Content-Encoding"]
+            )
+            decompressed = decoder.decompress(self._raw_content)
+            return decompressed
+
+        return self._raw_content
+
+
+class _Response(transport.Response):
+    """
+    Requests transport response adapter.
+
+    Args:
+        response (requests.Response): The raw Requests response.
+    """
+
+    def __init__(self, response):
+        self._response = response
+
+    @property
+    def status(self):
+        return self._response.status
+
+    @property
+    def headers(self):
+        return self._response.headers
+
+    @property
+    def data(self):
+        return self._response.content
+
+
+class Request(transport.Request):
+    """Requests request adapter.
+
+    This class is used internally for making requests using asyncio transports
+    in a consistent way. If you use :class:`AuthorizedSession` you do not need
+    to construct or use this class directly.
+
+    This class can be useful if you want to manually refresh a
+    :class:`~google.auth.credentials.Credentials` instance::
+
+        import google.auth.transport.aiohttp_requests
+
+        request = google.auth.transport.aiohttp_requests.Request()
+
+        credentials.refresh(request)
+
+    Args:
+        session (aiohttp.ClientSession): An instance :class:`aiohttp.ClientSession` used
+            to make HTTP requests. If not specified, a session will be created.
+
+    .. automethod:: __call__
+    """
+
+    def __init__(self, session=None):
+        # TODO: Use auto_decompress property for aiohttp 3.7+
+        if session is not None and session._auto_decompress:
+            raise ValueError(
+                "Client sessions with auto_decompress=True are not supported."
+            )
+        self.session = session
+
+    async def __call__(
+        self,
+        url,
+        method="GET",
+        body=None,
+        headers=None,
+        timeout=_DEFAULT_TIMEOUT,
+        **kwargs,
+    ):
+        """
+        Make an HTTP request using aiohttp.
+
+        Args:
+            url (str): The URL to be requested.
+            method (Optional[str]):
+                The HTTP method to use for the request. Defaults to 'GET'.
+            body (Optional[bytes]):
+                The payload or body in HTTP request.
+            headers (Optional[Mapping[str, str]]):
+                Request headers.
+            timeout (Optional[int]): The number of seconds to wait for a
+                response from the server. If not specified or if None, the
+                requests default timeout will be used.
+            kwargs: Additional arguments passed through to the underlying
+                requests :meth:`requests.Session.request` method.
+
+        Returns:
+            google.auth.transport.Response: The HTTP response.
+
+        Raises:
+            google.auth.exceptions.TransportError: If any exception occurred.
+        """
+
+        try:
+            if self.session is None:  # pragma: NO COVER
+                self.session = aiohttp.ClientSession(
+                    auto_decompress=False
+                )  # pragma: NO COVER
+            requests._LOGGER.debug("Making request: %s %s", method, url)
+            response = await self.session.request(
+                method, url, data=body, headers=headers, timeout=timeout, **kwargs
+            )
+            return _CombinedResponse(response)
+
+        except aiohttp.ClientError as caught_exc:
+            new_exc = exceptions.TransportError(caught_exc)
+            six.raise_from(new_exc, caught_exc)
+
+        except asyncio.TimeoutError as caught_exc:
+            new_exc = exceptions.TransportError(caught_exc)
+            six.raise_from(new_exc, caught_exc)
+
+
+class AuthorizedSession(aiohttp.ClientSession):
+    """This is an async implementation of the Authorized Session class. We utilize an
+    aiohttp transport instance, and the interface mirrors the google.auth.transport.requests
+    Authorized Session class, except for the change in the transport used in the async use case.
+
+    A Requests Session class with credentials.
+
+    This class is used to perform requests to API endpoints that require
+    authorization::
+
+        from google.auth.transport import aiohttp_requests
+
+        async with aiohttp_requests.AuthorizedSession(credentials) as authed_session:
+            response = await authed_session.request(
+                'GET', 'https://www.googleapis.com/storage/v1/b')
+
+    The underlying :meth:`request` implementation handles adding the
+    credentials' headers to the request and refreshing credentials as needed.
+
+    Args:
+        credentials (google.auth._credentials_async.Credentials):
+            The credentials to add to the request.
+        refresh_status_codes (Sequence[int]): Which HTTP status codes indicate
+            that credentials should be refreshed and the request should be
+            retried.
+        max_refresh_attempts (int): The maximum number of times to attempt to
+            refresh the credentials and retry the request.
+        refresh_timeout (Optional[int]): The timeout value in seconds for
+            credential refresh HTTP requests.
+        auth_request (google.auth.transport.aiohttp_requests.Request):
+            (Optional) An instance of
+            :class:`~google.auth.transport.aiohttp_requests.Request` used when
+            refreshing credentials. If not passed,
+            an instance of :class:`~google.auth.transport.aiohttp_requests.Request`
+            is created.
+    """
+
+    def __init__(
+        self,
+        credentials,
+        refresh_status_codes=transport.DEFAULT_REFRESH_STATUS_CODES,
+        max_refresh_attempts=transport.DEFAULT_MAX_REFRESH_ATTEMPTS,
+        refresh_timeout=None,
+        auth_request=None,
+        auto_decompress=False,
+    ):
+        super(AuthorizedSession, self).__init__()
+        self.credentials = credentials
+        self._refresh_status_codes = refresh_status_codes
+        self._max_refresh_attempts = max_refresh_attempts
+        self._refresh_timeout = refresh_timeout
+        self._is_mtls = False
+        self._auth_request = auth_request
+        self._auth_request_session = None
+        self._loop = asyncio.get_event_loop()
+        self._refresh_lock = asyncio.Lock()
+        self._auto_decompress = auto_decompress
+
+    async def request(
+        self,
+        method,
+        url,
+        data=None,
+        headers=None,
+        max_allowed_time=None,
+        timeout=_DEFAULT_TIMEOUT,
+        auto_decompress=False,
+        **kwargs,
+    ):
+
+        """Implementation of Authorized Session aiohttp request.
+
+        Args:
+            method (str):
+                The http request method used (e.g. GET, PUT, DELETE)
+            url (str):
+                The url at which the http request is sent.
+            data (Optional[dict]): Dictionary, list of tuples, bytes, or file-like
+                object to send in the body of the Request.
+            headers (Optional[dict]): Dictionary of HTTP Headers to send with the
+                Request.
+            timeout (Optional[Union[float, aiohttp.ClientTimeout]]):
+                The amount of time in seconds to wait for the server response
+                with each individual request. Can also be passed as an
+                ``aiohttp.ClientTimeout`` object.
+            max_allowed_time (Optional[float]):
+                If the method runs longer than this, a ``Timeout`` exception is
+                automatically raised. Unlike the ``timeout`` parameter, this
+                value applies to the total method execution time, even if
+                multiple requests are made under the hood.
+
+                Mind that it is not guaranteed that the timeout error is raised
+                at ``max_allowed_time``. It might take longer, for example, if
+                an underlying request takes a lot of time, but the request
+                itself does not timeout, e.g. if a large file is being
+                transmitted. The timout error will be raised after such
+                request completes.
+        """
+        # Headers come in as bytes which isn't expected behavior, the resumable
+        # media libraries in some cases expect a str type for the header values,
+        # but sometimes the operations return these in bytes types.
+        if headers:
+            for key in headers.keys():
+                if type(headers[key]) is bytes:
+                    headers[key] = headers[key].decode("utf-8")
+
+        async with aiohttp.ClientSession(
+            auto_decompress=self._auto_decompress
+        ) as self._auth_request_session:
+            auth_request = Request(self._auth_request_session)
+            self._auth_request = auth_request
+
+            # Use a kwarg for this instead of an attribute to maintain
+            # thread-safety.
+            _credential_refresh_attempt = kwargs.pop("_credential_refresh_attempt", 0)
+            # Make a copy of the headers. They will be modified by the credentials
+            # and we want to pass the original headers if we recurse.
+            request_headers = headers.copy() if headers is not None else {}
+
+            # Do not apply the timeout unconditionally in order to not override the
+            # _auth_request's default timeout.
+            auth_request = (
+                self._auth_request
+                if timeout is None
+                else functools.partial(self._auth_request, timeout=timeout)
+            )
+
+            remaining_time = max_allowed_time
+
+            with requests.TimeoutGuard(remaining_time, asyncio.TimeoutError) as guard:
+                await self.credentials.before_request(
+                    auth_request, method, url, request_headers
+                )
+
+            with requests.TimeoutGuard(remaining_time, asyncio.TimeoutError) as guard:
+                response = await super(AuthorizedSession, self).request(
+                    method,
+                    url,
+                    data=data,
+                    headers=request_headers,
+                    timeout=timeout,
+                    **kwargs,
+                )
+
+            remaining_time = guard.remaining_timeout
+
+            if (
+                response.status in self._refresh_status_codes
+                and _credential_refresh_attempt < self._max_refresh_attempts
+            ):
+
+                requests._LOGGER.info(
+                    "Refreshing credentials due to a %s response. Attempt %s/%s.",
+                    response.status,
+                    _credential_refresh_attempt + 1,
+                    self._max_refresh_attempts,
+                )
+
+                # Do not apply the timeout unconditionally in order to not override the
+                # _auth_request's default timeout.
+                auth_request = (
+                    self._auth_request
+                    if timeout is None
+                    else functools.partial(self._auth_request, timeout=timeout)
+                )
+
+                with requests.TimeoutGuard(
+                    remaining_time, asyncio.TimeoutError
+                ) as guard:
+                    async with self._refresh_lock:
+                        await self._loop.run_in_executor(
+                            None, self.credentials.refresh, auth_request
+                        )
+
+                remaining_time = guard.remaining_timeout
+
+                return await self.request(
+                    method,
+                    url,
+                    data=data,
+                    headers=headers,
+                    max_allowed_time=remaining_time,
+                    timeout=timeout,
+                    _credential_refresh_attempt=_credential_refresh_attempt + 1,
+                    **kwargs,
+                )
+
+        return response
diff --git a/google/auth/transport/_http_client.py b/google/auth/transport/_http_client.py
new file mode 100644
index 0000000..c153763
--- /dev/null
+++ b/google/auth/transport/_http_client.py
@@ -0,0 +1,115 @@
+# Copyright 2016 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.
+
+"""Transport adapter for http.client, for internal use only."""
+
+import logging
+import socket
+
+import six
+from six.moves import http_client
+from six.moves import urllib
+
+from google.auth import exceptions
+from google.auth import transport
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class Response(transport.Response):
+    """http.client transport response adapter.
+
+    Args:
+        response (http.client.HTTPResponse): The raw http client response.
+    """
+
+    def __init__(self, response):
+        self._status = response.status
+        self._headers = {key.lower(): value for key, value in response.getheaders()}
+        self._data = response.read()
+
+    @property
+    def status(self):
+        return self._status
+
+    @property
+    def headers(self):
+        return self._headers
+
+    @property
+    def data(self):
+        return self._data
+
+
+class Request(transport.Request):
+    """http.client transport request adapter."""
+
+    def __call__(
+        self, url, method="GET", body=None, headers=None, timeout=None, **kwargs
+    ):
+        """Make an HTTP request using http.client.
+
+        Args:
+            url (str): The URI to be requested.
+            method (str): The HTTP method to use for the request. Defaults
+                to 'GET'.
+            body (bytes): The payload / body in HTTP request.
+            headers (Mapping): Request headers.
+            timeout (Optional(int)): The number of seconds to wait for a
+                response from the server. If not specified or if None, the
+                socket global default timeout will be used.
+            kwargs: Additional arguments passed throught to the underlying
+                :meth:`~http.client.HTTPConnection.request` method.
+
+        Returns:
+            Response: The HTTP response.
+
+        Raises:
+            google.auth.exceptions.TransportError: If any exception occurred.
+        """
+        # socket._GLOBAL_DEFAULT_TIMEOUT is the default in http.client.
+        if timeout is None:
+            timeout = socket._GLOBAL_DEFAULT_TIMEOUT
+
+        # http.client doesn't allow None as the headers argument.
+        if headers is None:
+            headers = {}
+
+        # http.client needs the host and path parts specified separately.
+        parts = urllib.parse.urlsplit(url)
+        path = urllib.parse.urlunsplit(
+            ("", "", parts.path, parts.query, parts.fragment)
+        )
+
+        if parts.scheme != "http":
+            raise exceptions.TransportError(
+                "http.client transport only supports the http scheme, {}"
+                "was specified".format(parts.scheme)
+            )
+
+        connection = http_client.HTTPConnection(parts.netloc, timeout=timeout)
+
+        try:
+            _LOGGER.debug("Making request: %s %s", method, url)
+
+            connection.request(method, path, body=body, headers=headers, **kwargs)
+            response = connection.getresponse()
+            return Response(response)
+
+        except (http_client.HTTPException, socket.error) as caught_exc:
+            new_exc = exceptions.TransportError(caught_exc)
+            six.raise_from(new_exc, caught_exc)
+
+        finally:
+            connection.close()
diff --git a/google/auth/transport/_mtls_helper.py b/google/auth/transport/_mtls_helper.py
new file mode 100644
index 0000000..4dccb10
--- /dev/null
+++ b/google/auth/transport/_mtls_helper.py
@@ -0,0 +1,254 @@
+# 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.
+
+"""Helper functions for getting mTLS cert and key."""
+
+import json
+import logging
+from os import path
+import re
+import subprocess
+
+import six
+
+from google.auth import exceptions
+
+CONTEXT_AWARE_METADATA_PATH = "~/.secureConnect/context_aware_metadata.json"
+_CERT_PROVIDER_COMMAND = "cert_provider_command"
+_CERT_REGEX = re.compile(
+    b"-----BEGIN CERTIFICATE-----.+-----END CERTIFICATE-----\r?\n?", re.DOTALL
+)
+
+# support various format of key files, e.g.
+# "-----BEGIN PRIVATE KEY-----...",
+# "-----BEGIN EC PRIVATE KEY-----...",
+# "-----BEGIN RSA PRIVATE KEY-----..."
+# "-----BEGIN ENCRYPTED PRIVATE KEY-----"
+_KEY_REGEX = re.compile(
+    b"-----BEGIN [A-Z ]*PRIVATE KEY-----.+-----END [A-Z ]*PRIVATE KEY-----\r?\n?",
+    re.DOTALL,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+_PASSPHRASE_REGEX = re.compile(
+    b"-----BEGIN PASSPHRASE-----(.+)-----END PASSPHRASE-----", re.DOTALL
+)
+
+
+def _check_dca_metadata_path(metadata_path):
+    """Checks for context aware metadata. If it exists, returns the absolute path;
+    otherwise returns None.
+
+    Args:
+        metadata_path (str): context aware metadata path.
+
+    Returns:
+        str: absolute path if exists and None otherwise.
+    """
+    metadata_path = path.expanduser(metadata_path)
+    if not path.exists(metadata_path):
+        _LOGGER.debug("%s is not found, skip client SSL authentication.", metadata_path)
+        return None
+    return metadata_path
+
+
+def _read_dca_metadata_file(metadata_path):
+    """Loads context aware metadata from the given path.
+
+    Args:
+        metadata_path (str): context aware metadata path.
+
+    Returns:
+        Dict[str, str]: The metadata.
+
+    Raises:
+        google.auth.exceptions.ClientCertError: If failed to parse metadata as JSON.
+    """
+    try:
+        with open(metadata_path) as f:
+            metadata = json.load(f)
+    except ValueError as caught_exc:
+        new_exc = exceptions.ClientCertError(caught_exc)
+        six.raise_from(new_exc, caught_exc)
+
+    return metadata
+
+
+def _run_cert_provider_command(command, expect_encrypted_key=False):
+    """Run the provided command, and return client side mTLS cert, key and
+    passphrase.
+
+    Args:
+        command (List[str]): cert provider command.
+        expect_encrypted_key (bool): If encrypted private key is expected.
+
+    Returns:
+        Tuple[bytes, bytes, bytes]: client certificate bytes in PEM format, key
+            bytes in PEM format and passphrase bytes.
+
+    Raises:
+        google.auth.exceptions.ClientCertError: if problems occurs when running
+            the cert provider command or generating cert, key and passphrase.
+    """
+    try:
+        process = subprocess.Popen(
+            command, stdout=subprocess.PIPE, stderr=subprocess.PIPE
+        )
+        stdout, stderr = process.communicate()
+    except OSError as caught_exc:
+        new_exc = exceptions.ClientCertError(caught_exc)
+        six.raise_from(new_exc, caught_exc)
+
+    # Check cert provider command execution error.
+    if process.returncode != 0:
+        raise exceptions.ClientCertError(
+            "Cert provider command returns non-zero status code %s" % process.returncode
+        )
+
+    # Extract certificate (chain), key and passphrase.
+    cert_match = re.findall(_CERT_REGEX, stdout)
+    if len(cert_match) != 1:
+        raise exceptions.ClientCertError("Client SSL certificate is missing or invalid")
+    key_match = re.findall(_KEY_REGEX, stdout)
+    if len(key_match) != 1:
+        raise exceptions.ClientCertError("Client SSL key is missing or invalid")
+    passphrase_match = re.findall(_PASSPHRASE_REGEX, stdout)
+
+    if expect_encrypted_key:
+        if len(passphrase_match) != 1:
+            raise exceptions.ClientCertError("Passphrase is missing or invalid")
+        if b"ENCRYPTED" not in key_match[0]:
+            raise exceptions.ClientCertError("Encrypted private key is expected")
+        return cert_match[0], key_match[0], passphrase_match[0].strip()
+
+    if b"ENCRYPTED" in key_match[0]:
+        raise exceptions.ClientCertError("Encrypted private key is not expected")
+    if len(passphrase_match) > 0:
+        raise exceptions.ClientCertError("Passphrase is not expected")
+    return cert_match[0], key_match[0], None
+
+
+def get_client_ssl_credentials(
+    generate_encrypted_key=False,
+    context_aware_metadata_path=CONTEXT_AWARE_METADATA_PATH,
+):
+    """Returns the client side certificate, private key and passphrase.
+
+    Args:
+        generate_encrypted_key (bool): If set to True, encrypted private key
+            and passphrase will be generated; otherwise, unencrypted private key
+            will be generated and passphrase will be None.
+        context_aware_metadata_path (str): The context_aware_metadata.json file path.
+
+    Returns:
+        Tuple[bool, bytes, bytes, bytes]:
+            A boolean indicating if cert, key and passphrase are obtained, the
+            cert bytes and key bytes both in PEM format, and passphrase bytes.
+
+    Raises:
+        google.auth.exceptions.ClientCertError: if problems occurs when getting
+            the cert, key and passphrase.
+    """
+    metadata_path = _check_dca_metadata_path(context_aware_metadata_path)
+
+    if metadata_path:
+        metadata_json = _read_dca_metadata_file(metadata_path)
+
+        if _CERT_PROVIDER_COMMAND not in metadata_json:
+            raise exceptions.ClientCertError("Cert provider command is not found")
+
+        command = metadata_json[_CERT_PROVIDER_COMMAND]
+
+        if generate_encrypted_key and "--with_passphrase" not in command:
+            command.append("--with_passphrase")
+
+        # Execute the command.
+        cert, key, passphrase = _run_cert_provider_command(
+            command, expect_encrypted_key=generate_encrypted_key
+        )
+        return True, cert, key, passphrase
+
+    return False, None, None, None
+
+
+def get_client_cert_and_key(client_cert_callback=None):
+    """Returns the client side certificate and private key. The function first
+    tries to get certificate and key from client_cert_callback; if the callback
+    is None or doesn't provide certificate and key, the function tries application
+    default SSL credentials.
+
+    Args:
+        client_cert_callback (Optional[Callable[[], (bytes, bytes)]]): An
+            optional callback which returns client certificate bytes and private
+            key bytes both in PEM format.
+
+    Returns:
+        Tuple[bool, bytes, bytes]:
+            A boolean indicating if cert and key are obtained, the cert bytes
+            and key bytes both in PEM format.
+
+    Raises:
+        google.auth.exceptions.ClientCertError: if problems occurs when getting
+            the cert and key.
+    """
+    if client_cert_callback:
+        cert, key = client_cert_callback()
+        return True, cert, key
+
+    has_cert, cert, key, _ = get_client_ssl_credentials(generate_encrypted_key=False)
+    return has_cert, cert, key
+
+
+def decrypt_private_key(key, passphrase):
+    """A helper function to decrypt the private key with the given passphrase.
+    google-auth library doesn't support passphrase protected private key for
+    mutual TLS channel. This helper function can be used to decrypt the
+    passphrase protected private key in order to estalish mutual TLS channel.
+
+    For example, if you have a function which produces client cert, passphrase
+    protected private key and passphrase, you can convert it to a client cert
+    callback function accepted by google-auth::
+
+        from google.auth.transport import _mtls_helper
+
+        def your_client_cert_function():
+            return cert, encrypted_key, passphrase
+
+        # callback accepted by google-auth for mutual TLS channel.
+        def client_cert_callback():
+            cert, encrypted_key, passphrase = your_client_cert_function()
+            decrypted_key = _mtls_helper.decrypt_private_key(encrypted_key,
+                passphrase)
+            return cert, decrypted_key
+
+    Args:
+        key (bytes): The private key bytes in PEM format.
+        passphrase (bytes): The passphrase bytes.
+
+    Returns:
+        bytes: The decrypted private key in PEM format.
+
+    Raises:
+        ImportError: If pyOpenSSL is not installed.
+        OpenSSL.crypto.Error: If there is any problem decrypting the private key.
+    """
+    from OpenSSL import crypto
+
+    # First convert encrypted_key_bytes to PKey object
+    pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key, passphrase=passphrase)
+
+    # Then dump the decrypted key bytes
+    return crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)
diff --git a/google/auth/transport/grpc.py b/google/auth/transport/grpc.py
new file mode 100644
index 0000000..c47cb3d
--- /dev/null
+++ b/google/auth/transport/grpc.py
@@ -0,0 +1,349 @@
+# Copyright 2016 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.
+
+"""Authorization support for gRPC."""
+
+from __future__ import absolute_import
+
+import logging
+import os
+
+import six
+
+from google.auth import environment_vars
+from google.auth import exceptions
+from google.auth.transport import _mtls_helper
+from google.oauth2 import service_account
+
+try:
+    import grpc
+except ImportError as caught_exc:  # pragma: NO COVER
+    six.raise_from(
+        ImportError(
+            "gRPC is not installed, please install the grpcio package "
+            "to use the gRPC transport."
+        ),
+        caught_exc,
+    )
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class AuthMetadataPlugin(grpc.AuthMetadataPlugin):
+    """A `gRPC AuthMetadataPlugin`_ that inserts the credentials into each
+    request.
+
+    .. _gRPC AuthMetadataPlugin:
+        http://www.grpc.io/grpc/python/grpc.html#grpc.AuthMetadataPlugin
+
+    Args:
+        credentials (google.auth.credentials.Credentials): The credentials to
+            add to requests.
+        request (google.auth.transport.Request): A HTTP transport request
+            object used to refresh credentials as needed.
+        default_host (Optional[str]): A host like "pubsub.googleapis.com".
+            This is used when a self-signed JWT is created from service
+            account credentials.
+    """
+
+    def __init__(self, credentials, request, default_host=None):
+        # pylint: disable=no-value-for-parameter
+        # pylint doesn't realize that the super method takes no arguments
+        # because this class is the same name as the superclass.
+        super(AuthMetadataPlugin, self).__init__()
+        self._credentials = credentials
+        self._request = request
+        self._default_host = default_host
+
+    def _get_authorization_headers(self, context):
+        """Gets the authorization headers for a request.
+
+        Returns:
+            Sequence[Tuple[str, str]]: A list of request headers (key, value)
+                to add to the request.
+        """
+        headers = {}
+
+        # https://google.aip.dev/auth/4111
+        # Attempt to use self-signed JWTs when a service account is used.
+        # A default host must be explicitly provided since it cannot always
+        # be determined from the context.service_url.
+        if isinstance(self._credentials, service_account.Credentials):
+            self._credentials._create_self_signed_jwt(
+                "https://{}/".format(self._default_host) if self._default_host else None
+            )
+
+        self._credentials.before_request(
+            self._request, context.method_name, context.service_url, headers
+        )
+
+        return list(six.iteritems(headers))
+
+    def __call__(self, context, callback):
+        """Passes authorization metadata into the given callback.
+
+        Args:
+            context (grpc.AuthMetadataContext): The RPC context.
+            callback (grpc.AuthMetadataPluginCallback): The callback that will
+                be invoked to pass in the authorization metadata.
+        """
+        callback(self._get_authorization_headers(context), None)
+
+
+def secure_authorized_channel(
+    credentials,
+    request,
+    target,
+    ssl_credentials=None,
+    client_cert_callback=None,
+    **kwargs
+):
+    """Creates a secure authorized gRPC channel.
+
+    This creates a channel with SSL and :class:`AuthMetadataPlugin`. This
+    channel can be used to create a stub that can make authorized requests.
+    Users can configure client certificate or rely on device certificates to
+    establish a mutual TLS channel, if the `GOOGLE_API_USE_CLIENT_CERTIFICATE`
+    variable is explicitly set to `true`.
+
+    Example::
+
+        import google.auth
+        import google.auth.transport.grpc
+        import google.auth.transport.requests
+        from google.cloud.speech.v1 import cloud_speech_pb2
+
+        # Get credentials.
+        credentials, _ = google.auth.default()
+
+        # Get an HTTP request function to refresh credentials.
+        request = google.auth.transport.requests.Request()
+
+        # Create a channel.
+        channel = google.auth.transport.grpc.secure_authorized_channel(
+            credentials, regular_endpoint, request,
+            ssl_credentials=grpc.ssl_channel_credentials())
+
+        # Use the channel to create a stub.
+        cloud_speech.create_Speech_stub(channel)
+
+    Usage:
+
+    There are actually a couple of options to create a channel, depending on if
+    you want to create a regular or mutual TLS channel.
+
+    First let's list the endpoints (regular vs mutual TLS) to choose from::
+
+        regular_endpoint = 'speech.googleapis.com:443'
+        mtls_endpoint = 'speech.mtls.googleapis.com:443'
+
+    Option 1: create a regular (non-mutual) TLS channel by explicitly setting
+    the ssl_credentials::
+
+        regular_ssl_credentials = grpc.ssl_channel_credentials()
+
+        channel = google.auth.transport.grpc.secure_authorized_channel(
+            credentials, regular_endpoint, request,
+            ssl_credentials=regular_ssl_credentials)
+
+    Option 2: create a mutual TLS channel by calling a callback which returns
+    the client side certificate and the key (Note that
+    `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be explicitly
+    set to `true`)::
+
+        def my_client_cert_callback():
+            code_to_load_client_cert_and_key()
+            if loaded:
+                return (pem_cert_bytes, pem_key_bytes)
+            raise MyClientCertFailureException()
+
+        try:
+            channel = google.auth.transport.grpc.secure_authorized_channel(
+                credentials, mtls_endpoint, request,
+                client_cert_callback=my_client_cert_callback)
+        except MyClientCertFailureException:
+            # handle the exception
+
+    Option 3: use application default SSL credentials. It searches and uses
+    the command in a context aware metadata file, which is available on devices
+    with endpoint verification support (Note that
+    `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be explicitly
+    set to `true`).
+    See https://cloud.google.com/endpoint-verification/docs/overview::
+
+        try:
+            default_ssl_credentials = SslCredentials()
+        except:
+            # Exception can be raised if the context aware metadata is malformed.
+            # See :class:`SslCredentials` for the possible exceptions.
+
+        # Choose the endpoint based on the SSL credentials type.
+        if default_ssl_credentials.is_mtls:
+            endpoint_to_use = mtls_endpoint
+        else:
+            endpoint_to_use = regular_endpoint
+        channel = google.auth.transport.grpc.secure_authorized_channel(
+            credentials, endpoint_to_use, request,
+            ssl_credentials=default_ssl_credentials)
+
+    Option 4: not setting ssl_credentials and client_cert_callback. For devices
+    without endpoint verification support or `GOOGLE_API_USE_CLIENT_CERTIFICATE`
+    environment variable is not `true`, a regular TLS channel is created;
+    otherwise, a mutual TLS channel is created, however, the call should be
+    wrapped in a try/except block in case of malformed context aware metadata.
+
+    The following code uses regular_endpoint, it works the same no matter the
+    created channle is regular or mutual TLS. Regular endpoint ignores client
+    certificate and key::
+
+        channel = google.auth.transport.grpc.secure_authorized_channel(
+            credentials, regular_endpoint, request)
+
+    The following code uses mtls_endpoint, if the created channle is regular,
+    and API mtls_endpoint is confgured to require client SSL credentials, API
+    calls using this channel will be rejected::
+
+        channel = google.auth.transport.grpc.secure_authorized_channel(
+            credentials, mtls_endpoint, request)
+
+    Args:
+        credentials (google.auth.credentials.Credentials): The credentials to
+            add to requests.
+        request (google.auth.transport.Request): A HTTP transport request
+            object used to refresh credentials as needed. Even though gRPC
+            is a separate transport, there's no way to refresh the credentials
+            without using a standard http transport.
+        target (str): The host and port of the service.
+        ssl_credentials (grpc.ChannelCredentials): Optional SSL channel
+            credentials. This can be used to specify different certificates.
+            This argument is mutually exclusive with client_cert_callback;
+            providing both will raise an exception.
+            If ssl_credentials and client_cert_callback are None, application
+            default SSL credentials are used if `GOOGLE_API_USE_CLIENT_CERTIFICATE`
+            environment variable is explicitly set to `true`, otherwise one way TLS
+            SSL credentials are used.
+        client_cert_callback (Callable[[], (bytes, bytes)]): Optional
+            callback function to obtain client certicate and key for mutual TLS
+            connection. This argument is mutually exclusive with
+            ssl_credentials; providing both will raise an exception.
+            This argument does nothing unless `GOOGLE_API_USE_CLIENT_CERTIFICATE`
+            environment variable is explicitly set to `true`.
+        kwargs: Additional arguments to pass to :func:`grpc.secure_channel`.
+
+    Returns:
+        grpc.Channel: The created gRPC channel.
+
+    Raises:
+        google.auth.exceptions.MutualTLSChannelError: If mutual TLS channel
+            creation failed for any reason.
+    """
+    # Create the metadata plugin for inserting the authorization header.
+    metadata_plugin = AuthMetadataPlugin(credentials, request)
+
+    # Create a set of grpc.CallCredentials using the metadata plugin.
+    google_auth_credentials = grpc.metadata_call_credentials(metadata_plugin)
+
+    if ssl_credentials and client_cert_callback:
+        raise ValueError(
+            "Received both ssl_credentials and client_cert_callback; "
+            "these are mutually exclusive."
+        )
+
+    # If SSL credentials are not explicitly set, try client_cert_callback and ADC.
+    if not ssl_credentials:
+        use_client_cert = os.getenv(
+            environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE, "false"
+        )
+        if use_client_cert == "true" and client_cert_callback:
+            # Use the callback if provided.
+            cert, key = client_cert_callback()
+            ssl_credentials = grpc.ssl_channel_credentials(
+                certificate_chain=cert, private_key=key
+            )
+        elif use_client_cert == "true":
+            # Use application default SSL credentials.
+            adc_ssl_credentils = SslCredentials()
+            ssl_credentials = adc_ssl_credentils.ssl_credentials
+        else:
+            ssl_credentials = grpc.ssl_channel_credentials()
+
+    # Combine the ssl credentials and the authorization credentials.
+    composite_credentials = grpc.composite_channel_credentials(
+        ssl_credentials, google_auth_credentials
+    )
+
+    return grpc.secure_channel(target, composite_credentials, **kwargs)
+
+
+class SslCredentials:
+    """Class for application default SSL credentials.
+
+    The behavior is controlled by `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment
+    variable whose default value is `false`. Client certificate will not be used
+    unless the environment variable is explicitly set to `true`. See
+    https://google.aip.dev/auth/4114
+
+    If the environment variable is `true`, then for devices with endpoint verification
+    support, a device certificate will be automatically loaded and mutual TLS will
+    be established.
+    See https://cloud.google.com/endpoint-verification/docs/overview.
+    """
+
+    def __init__(self):
+        use_client_cert = os.getenv(
+            environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE, "false"
+        )
+        if use_client_cert != "true":
+            self._is_mtls = False
+        else:
+            # Load client SSL credentials.
+            metadata_path = _mtls_helper._check_dca_metadata_path(
+                _mtls_helper.CONTEXT_AWARE_METADATA_PATH
+            )
+            self._is_mtls = metadata_path is not None
+
+    @property
+    def ssl_credentials(self):
+        """Get the created SSL channel credentials.
+
+        For devices with endpoint verification support, if the device certificate
+        loading has any problems, corresponding exceptions will be raised. For
+        a device without endpoint verification support, no exceptions will be
+        raised.
+
+        Returns:
+            grpc.ChannelCredentials: The created grpc channel credentials.
+
+        Raises:
+            google.auth.exceptions.MutualTLSChannelError: If mutual TLS channel
+                creation failed for any reason.
+        """
+        if self._is_mtls:
+            try:
+                _, cert, key, _ = _mtls_helper.get_client_ssl_credentials()
+                self._ssl_credentials = grpc.ssl_channel_credentials(
+                    certificate_chain=cert, private_key=key
+                )
+            except exceptions.ClientCertError as caught_exc:
+                new_exc = exceptions.MutualTLSChannelError(caught_exc)
+                six.raise_from(new_exc, caught_exc)
+        else:
+            self._ssl_credentials = grpc.ssl_channel_credentials()
+
+        return self._ssl_credentials
+
+    @property
+    def is_mtls(self):
+        """Indicates if the created SSL channel credentials is mutual TLS."""
+        return self._is_mtls
diff --git a/google/auth/transport/mtls.py b/google/auth/transport/mtls.py
new file mode 100644
index 0000000..b40bfbe
--- /dev/null
+++ b/google/auth/transport/mtls.py
@@ -0,0 +1,105 @@
+# 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.
+
+"""Utilites for mutual TLS."""
+
+import six
+
+from google.auth import exceptions
+from google.auth.transport import _mtls_helper
+
+
+def has_default_client_cert_source():
+    """Check if default client SSL credentials exists on the device.
+
+    Returns:
+        bool: indicating if the default client cert source exists.
+    """
+    metadata_path = _mtls_helper._check_dca_metadata_path(
+        _mtls_helper.CONTEXT_AWARE_METADATA_PATH
+    )
+    return metadata_path is not None
+
+
+def default_client_cert_source():
+    """Get a callback which returns the default client SSL credentials.
+
+    Returns:
+        Callable[[], [bytes, bytes]]: A callback which returns the default
+            client certificate bytes and private key bytes, both in PEM format.
+
+    Raises:
+        google.auth.exceptions.DefaultClientCertSourceError: If the default
+            client SSL credentials don't exist or are malformed.
+    """
+    if not has_default_client_cert_source():
+        raise exceptions.MutualTLSChannelError(
+            "Default client cert source doesn't exist"
+        )
+
+    def callback():
+        try:
+            _, cert_bytes, key_bytes = _mtls_helper.get_client_cert_and_key()
+        except (OSError, RuntimeError, ValueError) as caught_exc:
+            new_exc = exceptions.MutualTLSChannelError(caught_exc)
+            six.raise_from(new_exc, caught_exc)
+
+        return cert_bytes, key_bytes
+
+    return callback
+
+
+def default_client_encrypted_cert_source(cert_path, key_path):
+    """Get a callback which returns the default encrpyted client SSL credentials.
+
+    Args:
+        cert_path (str): The cert file path. The default client certificate will
+            be written to this file when the returned callback is called.
+        key_path (str): The key file path. The default encrypted client key will
+            be written to this file when the returned callback is called.
+
+    Returns:
+        Callable[[], [str, str, bytes]]: A callback which generates the default
+            client certificate, encrpyted private key and passphrase. It writes
+            the certificate and private key into the cert_path and key_path, and
+            returns the cert_path, key_path and passphrase bytes.
+
+    Raises:
+        google.auth.exceptions.DefaultClientCertSourceError: If any problem
+            occurs when loading or saving the client certificate and key.
+    """
+    if not has_default_client_cert_source():
+        raise exceptions.MutualTLSChannelError(
+            "Default client encrypted cert source doesn't exist"
+        )
+
+    def callback():
+        try:
+            (
+                _,
+                cert_bytes,
+                key_bytes,
+                passphrase_bytes,
+            ) = _mtls_helper.get_client_ssl_credentials(generate_encrypted_key=True)
+            with open(cert_path, "wb") as cert_file:
+                cert_file.write(cert_bytes)
+            with open(key_path, "wb") as key_file:
+                key_file.write(key_bytes)
+        except (exceptions.ClientCertError, OSError) as caught_exc:
+            new_exc = exceptions.MutualTLSChannelError(caught_exc)
+            six.raise_from(new_exc, caught_exc)
+
+        return cert_path, key_path, passphrase_bytes
+
+    return callback
diff --git a/google/auth/transport/requests.py b/google/auth/transport/requests.py
new file mode 100644
index 0000000..817176b
--- /dev/null
+++ b/google/auth/transport/requests.py
@@ -0,0 +1,542 @@
+# Copyright 2016 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.
+
+"""Transport adapter for Requests."""
+
+from __future__ import absolute_import
+
+import functools
+import logging
+import numbers
+import os
+import time
+
+try:
+    import requests
+except ImportError as caught_exc:  # pragma: NO COVER
+    import six
+
+    six.raise_from(
+        ImportError(
+            "The requests library is not installed, please install the "
+            "requests package to use the requests transport."
+        ),
+        caught_exc,
+    )
+import requests.adapters  # pylint: disable=ungrouped-imports
+import requests.exceptions  # pylint: disable=ungrouped-imports
+from requests.packages.urllib3.util.ssl_ import (
+    create_urllib3_context,
+)  # pylint: disable=ungrouped-imports
+import six  # pylint: disable=ungrouped-imports
+
+from google.auth import environment_vars
+from google.auth import exceptions
+from google.auth import transport
+import google.auth.transport._mtls_helper
+from google.oauth2 import service_account
+
+_LOGGER = logging.getLogger(__name__)
+
+_DEFAULT_TIMEOUT = 120  # in seconds
+
+
+class _Response(transport.Response):
+    """Requests transport response adapter.
+
+    Args:
+        response (requests.Response): The raw Requests response.
+    """
+
+    def __init__(self, response):
+        self._response = response
+
+    @property
+    def status(self):
+        return self._response.status_code
+
+    @property
+    def headers(self):
+        return self._response.headers
+
+    @property
+    def data(self):
+        return self._response.content
+
+
+class TimeoutGuard(object):
+    """A context manager raising an error if the suite execution took too long.
+
+    Args:
+        timeout (Union[None, Union[float, Tuple[float, float]]]):
+            The maximum number of seconds a suite can run without the context
+            manager raising a timeout exception on exit. If passed as a tuple,
+            the smaller of the values is taken as a timeout. If ``None``, a
+            timeout error is never raised.
+        timeout_error_type (Optional[Exception]):
+            The type of the error to raise on timeout. Defaults to
+            :class:`requests.exceptions.Timeout`.
+    """
+
+    def __init__(self, timeout, timeout_error_type=requests.exceptions.Timeout):
+        self._timeout = timeout
+        self.remaining_timeout = timeout
+        self._timeout_error_type = timeout_error_type
+
+    def __enter__(self):
+        self._start = time.time()
+        return self
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        if exc_value:
+            return  # let the error bubble up automatically
+
+        if self._timeout is None:
+            return  # nothing to do, the timeout was not specified
+
+        elapsed = time.time() - self._start
+        deadline_hit = False
+
+        if isinstance(self._timeout, numbers.Number):
+            self.remaining_timeout = self._timeout - elapsed
+            deadline_hit = self.remaining_timeout <= 0
+        else:
+            self.remaining_timeout = tuple(x - elapsed for x in self._timeout)
+            deadline_hit = min(self.remaining_timeout) <= 0
+
+        if deadline_hit:
+            raise self._timeout_error_type()
+
+
+class Request(transport.Request):
+    """Requests request adapter.
+
+    This class is used internally for making requests using various transports
+    in a consistent way. If you use :class:`AuthorizedSession` you do not need
+    to construct or use this class directly.
+
+    This class can be useful if you want to manually refresh a
+    :class:`~google.auth.credentials.Credentials` instance::
+
+        import google.auth.transport.requests
+        import requests
+
+        request = google.auth.transport.requests.Request()
+
+        credentials.refresh(request)
+
+    Args:
+        session (requests.Session): An instance :class:`requests.Session` used
+            to make HTTP requests. If not specified, a session will be created.
+
+    .. automethod:: __call__
+    """
+
+    def __init__(self, session=None):
+        if not session:
+            session = requests.Session()
+
+        self.session = session
+
+    def __call__(
+        self,
+        url,
+        method="GET",
+        body=None,
+        headers=None,
+        timeout=_DEFAULT_TIMEOUT,
+        **kwargs
+    ):
+        """Make an HTTP request using requests.
+
+        Args:
+            url (str): The URI to be requested.
+            method (str): The HTTP method to use for the request. Defaults
+                to 'GET'.
+            body (bytes): The payload or body in HTTP request.
+            headers (Mapping[str, str]): Request headers.
+            timeout (Optional[int]): The number of seconds to wait for a
+                response from the server. If not specified or if None, the
+                requests default timeout will be used.
+            kwargs: Additional arguments passed through to the underlying
+                requests :meth:`~requests.Session.request` method.
+
+        Returns:
+            google.auth.transport.Response: The HTTP response.
+
+        Raises:
+            google.auth.exceptions.TransportError: If any exception occurred.
+        """
+        try:
+            _LOGGER.debug("Making request: %s %s", method, url)
+            response = self.session.request(
+                method, url, data=body, headers=headers, timeout=timeout, **kwargs
+            )
+            return _Response(response)
+        except requests.exceptions.RequestException as caught_exc:
+            new_exc = exceptions.TransportError(caught_exc)
+            six.raise_from(new_exc, caught_exc)
+
+
+class _MutualTlsAdapter(requests.adapters.HTTPAdapter):
+    """
+    A TransportAdapter that enables mutual TLS.
+
+    Args:
+        cert (bytes): client certificate in PEM format
+        key (bytes): client private key in PEM format
+
+    Raises:
+        ImportError: if certifi or pyOpenSSL is not installed
+        OpenSSL.crypto.Error: if client cert or key is invalid
+    """
+
+    def __init__(self, cert, key):
+        import certifi
+        from OpenSSL import crypto
+        import urllib3.contrib.pyopenssl
+
+        urllib3.contrib.pyopenssl.inject_into_urllib3()
+
+        pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key)
+        x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
+
+        ctx_poolmanager = create_urllib3_context()
+        ctx_poolmanager.load_verify_locations(cafile=certifi.where())
+        ctx_poolmanager._ctx.use_certificate(x509)
+        ctx_poolmanager._ctx.use_privatekey(pkey)
+        self._ctx_poolmanager = ctx_poolmanager
+
+        ctx_proxymanager = create_urllib3_context()
+        ctx_proxymanager.load_verify_locations(cafile=certifi.where())
+        ctx_proxymanager._ctx.use_certificate(x509)
+        ctx_proxymanager._ctx.use_privatekey(pkey)
+        self._ctx_proxymanager = ctx_proxymanager
+
+        super(_MutualTlsAdapter, self).__init__()
+
+    def init_poolmanager(self, *args, **kwargs):
+        kwargs["ssl_context"] = self._ctx_poolmanager
+        super(_MutualTlsAdapter, self).init_poolmanager(*args, **kwargs)
+
+    def proxy_manager_for(self, *args, **kwargs):
+        kwargs["ssl_context"] = self._ctx_proxymanager
+        return super(_MutualTlsAdapter, self).proxy_manager_for(*args, **kwargs)
+
+
+class AuthorizedSession(requests.Session):
+    """A Requests Session class with credentials.
+
+    This class is used to perform requests to API endpoints that require
+    authorization::
+
+        from google.auth.transport.requests import AuthorizedSession
+
+        authed_session = AuthorizedSession(credentials)
+
+        response = authed_session.request(
+            'GET', 'https://www.googleapis.com/storage/v1/b')
+
+
+    The underlying :meth:`request` implementation handles adding the
+    credentials' headers to the request and refreshing credentials as needed.
+
+    This class also supports mutual TLS via :meth:`configure_mtls_channel`
+    method. In order to use this method, the `GOOGLE_API_USE_CLIENT_CERTIFICATE`
+    environment variable must be explicitly set to ``true``, otherwise it does
+    nothing. Assume the environment is set to ``true``, the method behaves in the
+    following manner:
+
+    If client_cert_callback is provided, client certificate and private
+    key are loaded using the callback; if client_cert_callback is None,
+    application default SSL credentials will be used. Exceptions are raised if
+    there are problems with the certificate, private key, or the loading process,
+    so it should be called within a try/except block.
+
+    First we set the environment variable to ``true``, then create an :class:`AuthorizedSession`
+    instance and specify the endpoints::
+
+        regular_endpoint = 'https://pubsub.googleapis.com/v1/projects/{my_project_id}/topics'
+        mtls_endpoint = 'https://pubsub.mtls.googleapis.com/v1/projects/{my_project_id}/topics'
+
+        authed_session = AuthorizedSession(credentials)
+
+    Now we can pass a callback to :meth:`configure_mtls_channel`::
+
+        def my_cert_callback():
+            # some code to load client cert bytes and private key bytes, both in
+            # PEM format.
+            some_code_to_load_client_cert_and_key()
+            if loaded:
+                return cert, key
+            raise MyClientCertFailureException()
+
+        # Always call configure_mtls_channel within a try/except block.
+        try:
+            authed_session.configure_mtls_channel(my_cert_callback)
+        except:
+            # handle exceptions.
+
+        if authed_session.is_mtls:
+            response = authed_session.request('GET', mtls_endpoint)
+        else:
+            response = authed_session.request('GET', regular_endpoint)
+
+
+    You can alternatively use application default SSL credentials like this::
+
+        try:
+            authed_session.configure_mtls_channel()
+        except:
+            # handle exceptions.
+
+    Args:
+        credentials (google.auth.credentials.Credentials): The credentials to
+            add to the request.
+        refresh_status_codes (Sequence[int]): Which HTTP status codes indicate
+            that credentials should be refreshed and the request should be
+            retried.
+        max_refresh_attempts (int): The maximum number of times to attempt to
+            refresh the credentials and retry the request.
+        refresh_timeout (Optional[int]): The timeout value in seconds for
+            credential refresh HTTP requests.
+        auth_request (google.auth.transport.requests.Request):
+            (Optional) An instance of
+            :class:`~google.auth.transport.requests.Request` used when
+            refreshing credentials. If not passed,
+            an instance of :class:`~google.auth.transport.requests.Request`
+            is created.
+        default_host (Optional[str]): A host like "pubsub.googleapis.com".
+            This is used when a self-signed JWT is created from service
+            account credentials.
+    """
+
+    def __init__(
+        self,
+        credentials,
+        refresh_status_codes=transport.DEFAULT_REFRESH_STATUS_CODES,
+        max_refresh_attempts=transport.DEFAULT_MAX_REFRESH_ATTEMPTS,
+        refresh_timeout=None,
+        auth_request=None,
+        default_host=None,
+    ):
+        super(AuthorizedSession, self).__init__()
+        self.credentials = credentials
+        self._refresh_status_codes = refresh_status_codes
+        self._max_refresh_attempts = max_refresh_attempts
+        self._refresh_timeout = refresh_timeout
+        self._is_mtls = False
+        self._default_host = default_host
+
+        if auth_request is None:
+            self._auth_request_session = requests.Session()
+
+            # Using an adapter to make HTTP requests robust to network errors.
+            # This adapter retrys HTTP requests when network errors occur
+            # and the requests seems safely retryable.
+            retry_adapter = requests.adapters.HTTPAdapter(max_retries=3)
+            self._auth_request_session.mount("https://", retry_adapter)
+
+            # Do not pass `self` as the session here, as it can lead to
+            # infinite recursion.
+            auth_request = Request(self._auth_request_session)
+        else:
+            self._auth_request_session = None
+
+        # Request instance used by internal methods (for example,
+        # credentials.refresh).
+        self._auth_request = auth_request
+
+        # https://google.aip.dev/auth/4111
+        # Attempt to use self-signed JWTs when a service account is used.
+        if isinstance(self.credentials, service_account.Credentials):
+            self.credentials._create_self_signed_jwt(
+                "https://{}/".format(self._default_host) if self._default_host else None
+            )
+
+    def configure_mtls_channel(self, client_cert_callback=None):
+        """Configure the client certificate and key for SSL connection.
+
+        The function does nothing unless `GOOGLE_API_USE_CLIENT_CERTIFICATE` is
+        explicitly set to `true`. In this case if client certificate and key are
+        successfully obtained (from the given client_cert_callback or from application
+        default SSL credentials), a :class:`_MutualTlsAdapter` instance will be mounted
+        to "https://" prefix.
+
+        Args:
+            client_cert_callback (Optional[Callable[[], (bytes, bytes)]]):
+                The optional callback returns the client certificate and private
+                key bytes both in PEM format.
+                If the callback is None, application default SSL credentials
+                will be used.
+
+        Raises:
+            google.auth.exceptions.MutualTLSChannelError: If mutual TLS channel
+                creation failed for any reason.
+        """
+        use_client_cert = os.getenv(
+            environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE, "false"
+        )
+        if use_client_cert != "true":
+            self._is_mtls = False
+            return
+
+        try:
+            import OpenSSL
+        except ImportError as caught_exc:
+            new_exc = exceptions.MutualTLSChannelError(caught_exc)
+            six.raise_from(new_exc, caught_exc)
+
+        try:
+            (
+                self._is_mtls,
+                cert,
+                key,
+            ) = google.auth.transport._mtls_helper.get_client_cert_and_key(
+                client_cert_callback
+            )
+
+            if self._is_mtls:
+                mtls_adapter = _MutualTlsAdapter(cert, key)
+                self.mount("https://", mtls_adapter)
+        except (
+            exceptions.ClientCertError,
+            ImportError,
+            OpenSSL.crypto.Error,
+        ) as caught_exc:
+            new_exc = exceptions.MutualTLSChannelError(caught_exc)
+            six.raise_from(new_exc, caught_exc)
+
+    def request(
+        self,
+        method,
+        url,
+        data=None,
+        headers=None,
+        max_allowed_time=None,
+        timeout=_DEFAULT_TIMEOUT,
+        **kwargs
+    ):
+        """Implementation of Requests' request.
+
+        Args:
+            timeout (Optional[Union[float, Tuple[float, float]]]):
+                The amount of time in seconds to wait for the server response
+                with each individual request. Can also be passed as a tuple
+                ``(connect_timeout, read_timeout)``. See :meth:`requests.Session.request`
+                documentation for details.
+            max_allowed_time (Optional[float]):
+                If the method runs longer than this, a ``Timeout`` exception is
+                automatically raised. Unlike the ``timeout`` parameter, this
+                value applies to the total method execution time, even if
+                multiple requests are made under the hood.
+
+                Mind that it is not guaranteed that the timeout error is raised
+                at ``max_allowed_time``. It might take longer, for example, if
+                an underlying request takes a lot of time, but the request
+                itself does not timeout, e.g. if a large file is being
+                transmitted. The timout error will be raised after such
+                request completes.
+        """
+        # pylint: disable=arguments-differ
+        # Requests has a ton of arguments to request, but only two
+        # (method, url) are required. We pass through all of the other
+        # arguments to super, so no need to exhaustively list them here.
+
+        # Use a kwarg for this instead of an attribute to maintain
+        # thread-safety.
+        _credential_refresh_attempt = kwargs.pop("_credential_refresh_attempt", 0)
+
+        # Make a copy of the headers. They will be modified by the credentials
+        # and we want to pass the original headers if we recurse.
+        request_headers = headers.copy() if headers is not None else {}
+
+        # Do not apply the timeout unconditionally in order to not override the
+        # _auth_request's default timeout.
+        auth_request = (
+            self._auth_request
+            if timeout is None
+            else functools.partial(self._auth_request, timeout=timeout)
+        )
+
+        remaining_time = max_allowed_time
+
+        with TimeoutGuard(remaining_time) as guard:
+            self.credentials.before_request(auth_request, method, url, request_headers)
+        remaining_time = guard.remaining_timeout
+
+        with TimeoutGuard(remaining_time) as guard:
+            response = super(AuthorizedSession, self).request(
+                method,
+                url,
+                data=data,
+                headers=request_headers,
+                timeout=timeout,
+                **kwargs
+            )
+        remaining_time = guard.remaining_timeout
+
+        # If the response indicated that the credentials needed to be
+        # refreshed, then refresh the credentials and re-attempt the
+        # request.
+        # A stored token may expire between the time it is retrieved and
+        # the time the request is made, so we may need to try twice.
+        if (
+            response.status_code in self._refresh_status_codes
+            and _credential_refresh_attempt < self._max_refresh_attempts
+        ):
+
+            _LOGGER.info(
+                "Refreshing credentials due to a %s response. Attempt %s/%s.",
+                response.status_code,
+                _credential_refresh_attempt + 1,
+                self._max_refresh_attempts,
+            )
+
+            # Do not apply the timeout unconditionally in order to not override the
+            # _auth_request's default timeout.
+            auth_request = (
+                self._auth_request
+                if timeout is None
+                else functools.partial(self._auth_request, timeout=timeout)
+            )
+
+            with TimeoutGuard(remaining_time) as guard:
+                self.credentials.refresh(auth_request)
+            remaining_time = guard.remaining_timeout
+
+            # Recurse. Pass in the original headers, not our modified set, but
+            # do pass the adjusted max allowed time (i.e. the remaining total time).
+            return self.request(
+                method,
+                url,
+                data=data,
+                headers=headers,
+                max_allowed_time=remaining_time,
+                timeout=timeout,
+                _credential_refresh_attempt=_credential_refresh_attempt + 1,
+                **kwargs
+            )
+
+        return response
+
+    @property
+    def is_mtls(self):
+        """Indicates if the created SSL channel is mutual TLS."""
+        return self._is_mtls
+
+    def close(self):
+        if self._auth_request_session is not None:
+            self._auth_request_session.close()
+        super(AuthorizedSession, self).close()
diff --git a/google/auth/transport/urllib3.py b/google/auth/transport/urllib3.py
new file mode 100644
index 0000000..6a2504d
--- /dev/null
+++ b/google/auth/transport/urllib3.py
@@ -0,0 +1,439 @@
+# Copyright 2016 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.
+
+"""Transport adapter for urllib3."""
+
+from __future__ import absolute_import
+
+import logging
+import os
+import warnings
+
+# Certifi is Mozilla's certificate bundle. Urllib3 needs a certificate bundle
+# to verify HTTPS requests, and certifi is the recommended and most reliable
+# way to get a root certificate bundle. See
+# http://urllib3.readthedocs.io/en/latest/user-guide.html\
+#   #certificate-verification
+# For more details.
+try:
+    import certifi
+except ImportError:  # pragma: NO COVER
+    certifi = None
+
+try:
+    import urllib3
+except ImportError as caught_exc:  # pragma: NO COVER
+    import six
+
+    six.raise_from(
+        ImportError(
+            "The urllib3 library is not installed, please install the "
+            "urllib3 package to use the urllib3 transport."
+        ),
+        caught_exc,
+    )
+import six
+import urllib3.exceptions  # pylint: disable=ungrouped-imports
+
+from google.auth import environment_vars
+from google.auth import exceptions
+from google.auth import transport
+from google.oauth2 import service_account
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class _Response(transport.Response):
+    """urllib3 transport response adapter.
+
+    Args:
+        response (urllib3.response.HTTPResponse): The raw urllib3 response.
+    """
+
+    def __init__(self, response):
+        self._response = response
+
+    @property
+    def status(self):
+        return self._response.status
+
+    @property
+    def headers(self):
+        return self._response.headers
+
+    @property
+    def data(self):
+        return self._response.data
+
+
+class Request(transport.Request):
+    """urllib3 request adapter.
+
+    This class is used internally for making requests using various transports
+    in a consistent way. If you use :class:`AuthorizedHttp` you do not need
+    to construct or use this class directly.
+
+    This class can be useful if you want to manually refresh a
+    :class:`~google.auth.credentials.Credentials` instance::
+
+        import google.auth.transport.urllib3
+        import urllib3
+
+        http = urllib3.PoolManager()
+        request = google.auth.transport.urllib3.Request(http)
+
+        credentials.refresh(request)
+
+    Args:
+        http (urllib3.request.RequestMethods): An instance of any urllib3
+            class that implements :class:`~urllib3.request.RequestMethods`,
+            usually :class:`urllib3.PoolManager`.
+
+    .. automethod:: __call__
+    """
+
+    def __init__(self, http):
+        self.http = http
+
+    def __call__(
+        self, url, method="GET", body=None, headers=None, timeout=None, **kwargs
+    ):
+        """Make an HTTP request using urllib3.
+
+        Args:
+            url (str): The URI to be requested.
+            method (str): The HTTP method to use for the request. Defaults
+                to 'GET'.
+            body (bytes): The payload / body in HTTP request.
+            headers (Mapping[str, str]): Request headers.
+            timeout (Optional[int]): The number of seconds to wait for a
+                response from the server. If not specified or if None, the
+                urllib3 default timeout will be used.
+            kwargs: Additional arguments passed throught to the underlying
+                urllib3 :meth:`urlopen` method.
+
+        Returns:
+            google.auth.transport.Response: The HTTP response.
+
+        Raises:
+            google.auth.exceptions.TransportError: If any exception occurred.
+        """
+        # urllib3 uses a sentinel default value for timeout, so only set it if
+        # specified.
+        if timeout is not None:
+            kwargs["timeout"] = timeout
+
+        try:
+            _LOGGER.debug("Making request: %s %s", method, url)
+            response = self.http.request(
+                method, url, body=body, headers=headers, **kwargs
+            )
+            return _Response(response)
+        except urllib3.exceptions.HTTPError as caught_exc:
+            new_exc = exceptions.TransportError(caught_exc)
+            six.raise_from(new_exc, caught_exc)
+
+
+def _make_default_http():
+    if certifi is not None:
+        return urllib3.PoolManager(cert_reqs="CERT_REQUIRED", ca_certs=certifi.where())
+    else:
+        return urllib3.PoolManager()
+
+
+def _make_mutual_tls_http(cert, key):
+    """Create a mutual TLS HTTP connection with the given client cert and key.
+    See https://github.com/urllib3/urllib3/issues/474#issuecomment-253168415
+
+    Args:
+        cert (bytes): client certificate in PEM format
+        key (bytes): client private key in PEM format
+
+    Returns:
+        urllib3.PoolManager: Mutual TLS HTTP connection.
+
+    Raises:
+        ImportError: If certifi or pyOpenSSL is not installed.
+        OpenSSL.crypto.Error: If the cert or key is invalid.
+    """
+    import certifi
+    from OpenSSL import crypto
+    import urllib3.contrib.pyopenssl
+
+    urllib3.contrib.pyopenssl.inject_into_urllib3()
+    ctx = urllib3.util.ssl_.create_urllib3_context()
+    ctx.load_verify_locations(cafile=certifi.where())
+
+    pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key)
+    x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
+
+    ctx._ctx.use_certificate(x509)
+    ctx._ctx.use_privatekey(pkey)
+
+    http = urllib3.PoolManager(ssl_context=ctx)
+    return http
+
+
+class AuthorizedHttp(urllib3.request.RequestMethods):
+    """A urllib3 HTTP class with credentials.
+
+    This class is used to perform requests to API endpoints that require
+    authorization::
+
+        from google.auth.transport.urllib3 import AuthorizedHttp
+
+        authed_http = AuthorizedHttp(credentials)
+
+        response = authed_http.request(
+            'GET', 'https://www.googleapis.com/storage/v1/b')
+
+    This class implements :class:`urllib3.request.RequestMethods` and can be
+    used just like any other :class:`urllib3.PoolManager`.
+
+    The underlying :meth:`urlopen` implementation handles adding the
+    credentials' headers to the request and refreshing credentials as needed.
+
+    This class also supports mutual TLS via :meth:`configure_mtls_channel`
+    method. In order to use this method, the `GOOGLE_API_USE_CLIENT_CERTIFICATE`
+    environment variable must be explicitly set to `true`, otherwise it does
+    nothing. Assume the environment is set to `true`, the method behaves in the
+    following manner:
+    If client_cert_callback is provided, client certificate and private
+    key are loaded using the callback; if client_cert_callback is None,
+    application default SSL credentials will be used. Exceptions are raised if
+    there are problems with the certificate, private key, or the loading process,
+    so it should be called within a try/except block.
+
+    First we set the environment variable to `true`, then create an :class:`AuthorizedHttp`
+    instance and specify the endpoints::
+
+        regular_endpoint = 'https://pubsub.googleapis.com/v1/projects/{my_project_id}/topics'
+        mtls_endpoint = 'https://pubsub.mtls.googleapis.com/v1/projects/{my_project_id}/topics'
+
+        authed_http = AuthorizedHttp(credentials)
+
+    Now we can pass a callback to :meth:`configure_mtls_channel`::
+
+        def my_cert_callback():
+            # some code to load client cert bytes and private key bytes, both in
+            # PEM format.
+            some_code_to_load_client_cert_and_key()
+            if loaded:
+                return cert, key
+            raise MyClientCertFailureException()
+
+        # Always call configure_mtls_channel within a try/except block.
+        try:
+            is_mtls = authed_http.configure_mtls_channel(my_cert_callback)
+        except:
+            # handle exceptions.
+
+        if is_mtls:
+            response = authed_http.request('GET', mtls_endpoint)
+        else:
+            response = authed_http.request('GET', regular_endpoint)
+
+    You can alternatively use application default SSL credentials like this::
+
+        try:
+            is_mtls = authed_http.configure_mtls_channel()
+        except:
+            # handle exceptions.
+
+    Args:
+        credentials (google.auth.credentials.Credentials): The credentials to
+            add to the request.
+        http (urllib3.PoolManager): The underlying HTTP object to
+            use to make requests. If not specified, a
+            :class:`urllib3.PoolManager` instance will be constructed with
+            sane defaults.
+        refresh_status_codes (Sequence[int]): Which HTTP status codes indicate
+            that credentials should be refreshed and the request should be
+            retried.
+        max_refresh_attempts (int): The maximum number of times to attempt to
+            refresh the credentials and retry the request.
+        default_host (Optional[str]): A host like "pubsub.googleapis.com".
+            This is used when a self-signed JWT is created from service
+            account credentials.
+    """
+
+    def __init__(
+        self,
+        credentials,
+        http=None,
+        refresh_status_codes=transport.DEFAULT_REFRESH_STATUS_CODES,
+        max_refresh_attempts=transport.DEFAULT_MAX_REFRESH_ATTEMPTS,
+        default_host=None,
+    ):
+        if http is None:
+            self.http = _make_default_http()
+            self._has_user_provided_http = False
+        else:
+            self.http = http
+            self._has_user_provided_http = True
+
+        self.credentials = credentials
+        self._refresh_status_codes = refresh_status_codes
+        self._max_refresh_attempts = max_refresh_attempts
+        self._default_host = default_host
+        # Request instance used by internal methods (for example,
+        # credentials.refresh).
+        self._request = Request(self.http)
+
+        # https://google.aip.dev/auth/4111
+        # Attempt to use self-signed JWTs when a service account is used.
+        if isinstance(self.credentials, service_account.Credentials):
+            self.credentials._create_self_signed_jwt(
+                "https://{}/".format(self._default_host) if self._default_host else None
+            )
+
+        super(AuthorizedHttp, self).__init__()
+
+    def configure_mtls_channel(self, client_cert_callback=None):
+        """Configures mutual TLS channel using the given client_cert_callback or
+        application default SSL credentials. The behavior is controlled by
+        `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable.
+        (1) If the environment variable value is `true`, the function returns True
+        if the channel is mutual TLS and False otherwise. The `http` provided
+        in the constructor will be overwritten.
+        (2) If the environment variable is not set or `false`, the function does
+        nothing and it always return False.
+
+        Args:
+            client_cert_callback (Optional[Callable[[], (bytes, bytes)]]):
+                The optional callback returns the client certificate and private
+                key bytes both in PEM format.
+                If the callback is None, application default SSL credentials
+                will be used.
+
+        Returns:
+            True if the channel is mutual TLS and False otherwise.
+
+        Raises:
+            google.auth.exceptions.MutualTLSChannelError: If mutual TLS channel
+                creation failed for any reason.
+        """
+        use_client_cert = os.getenv(
+            environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE, "false"
+        )
+        if use_client_cert != "true":
+            return False
+
+        try:
+            import OpenSSL
+        except ImportError as caught_exc:
+            new_exc = exceptions.MutualTLSChannelError(caught_exc)
+            six.raise_from(new_exc, caught_exc)
+
+        try:
+            found_cert_key, cert, key = transport._mtls_helper.get_client_cert_and_key(
+                client_cert_callback
+            )
+
+            if found_cert_key:
+                self.http = _make_mutual_tls_http(cert, key)
+            else:
+                self.http = _make_default_http()
+        except (
+            exceptions.ClientCertError,
+            ImportError,
+            OpenSSL.crypto.Error,
+        ) as caught_exc:
+            new_exc = exceptions.MutualTLSChannelError(caught_exc)
+            six.raise_from(new_exc, caught_exc)
+
+        if self._has_user_provided_http:
+            self._has_user_provided_http = False
+            warnings.warn(
+                "`http` provided in the constructor is overwritten", UserWarning
+            )
+
+        return found_cert_key
+
+    def urlopen(self, method, url, body=None, headers=None, **kwargs):
+        """Implementation of urllib3's urlopen."""
+        # pylint: disable=arguments-differ
+        # We use kwargs to collect additional args that we don't need to
+        # introspect here. However, we do explicitly collect the two
+        # positional arguments.
+
+        # Use a kwarg for this instead of an attribute to maintain
+        # thread-safety.
+        _credential_refresh_attempt = kwargs.pop("_credential_refresh_attempt", 0)
+
+        if headers is None:
+            headers = self.headers
+
+        # Make a copy of the headers. They will be modified by the credentials
+        # and we want to pass the original headers if we recurse.
+        request_headers = headers.copy()
+
+        self.credentials.before_request(self._request, method, url, request_headers)
+
+        response = self.http.urlopen(
+            method, url, body=body, headers=request_headers, **kwargs
+        )
+
+        # If the response indicated that the credentials needed to be
+        # refreshed, then refresh the credentials and re-attempt the
+        # request.
+        # A stored token may expire between the time it is retrieved and
+        # the time the request is made, so we may need to try twice.
+        # The reason urllib3's retries aren't used is because they
+        # don't allow you to modify the request headers. :/
+        if (
+            response.status in self._refresh_status_codes
+            and _credential_refresh_attempt < self._max_refresh_attempts
+        ):
+
+            _LOGGER.info(
+                "Refreshing credentials due to a %s response. Attempt %s/%s.",
+                response.status,
+                _credential_refresh_attempt + 1,
+                self._max_refresh_attempts,
+            )
+
+            self.credentials.refresh(self._request)
+
+            # Recurse. Pass in the original headers, not our modified set.
+            return self.urlopen(
+                method,
+                url,
+                body=body,
+                headers=headers,
+                _credential_refresh_attempt=_credential_refresh_attempt + 1,
+                **kwargs
+            )
+
+        return response
+
+    # Proxy methods for compliance with the urllib3.PoolManager interface
+
+    def __enter__(self):
+        """Proxy to ``self.http``."""
+        return self.http.__enter__()
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        """Proxy to ``self.http``."""
+        return self.http.__exit__(exc_type, exc_val, exc_tb)
+
+    @property
+    def headers(self):
+        """Proxy to ``self.http``."""
+        return self.http.headers
+
+    @headers.setter
+    def headers(self, value):
+        """Proxy to ``self.http``."""
+        self.http.headers = value
diff --git a/google/auth/version.py b/google/auth/version.py
new file mode 100644
index 0000000..ad9a0c7
--- /dev/null
+++ b/google/auth/version.py
@@ -0,0 +1,15 @@
+# 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.
+
+__version__ = "2.3.3"
diff --git a/google/oauth2/__init__.py b/google/oauth2/__init__.py
new file mode 100644
index 0000000..4fb71fd
--- /dev/null
+++ b/google/oauth2/__init__.py
@@ -0,0 +1,15 @@
+# Copyright 2016 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.
+
+"""Google OAuth 2.0 Library for Python."""
diff --git a/google/oauth2/_client.py b/google/oauth2/_client.py
new file mode 100644
index 0000000..2f4e847
--- /dev/null
+++ b/google/oauth2/_client.py
@@ -0,0 +1,327 @@
+# Copyright 2016 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.
+
+"""OAuth 2.0 client.
+
+This is a client for interacting with an OAuth 2.0 authorization server's
+token endpoint.
+
+For more information about the token endpoint, see
+`Section 3.1 of rfc6749`_
+
+.. _Section 3.1 of rfc6749: https://tools.ietf.org/html/rfc6749#section-3.2
+"""
+
+import datetime
+import json
+
+import six
+from six.moves import http_client
+from six.moves import urllib
+
+from google.auth import _helpers
+from google.auth import exceptions
+from google.auth import jwt
+
+_URLENCODED_CONTENT_TYPE = "application/x-www-form-urlencoded"
+_JSON_CONTENT_TYPE = "application/json"
+_JWT_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer"
+_REFRESH_GRANT_TYPE = "refresh_token"
+
+
+def _handle_error_response(response_data):
+    """Translates an error response into an exception.
+
+    Args:
+        response_data (Mapping): The decoded response data.
+
+    Raises:
+        google.auth.exceptions.RefreshError: The errors contained in response_data.
+    """
+    try:
+        error_details = "{}: {}".format(
+            response_data["error"], response_data.get("error_description")
+        )
+    # If no details could be extracted, use the response data.
+    except (KeyError, ValueError):
+        error_details = json.dumps(response_data)
+
+    raise exceptions.RefreshError(error_details, response_data)
+
+
+def _parse_expiry(response_data):
+    """Parses the expiry field from a response into a datetime.
+
+    Args:
+        response_data (Mapping): The JSON-parsed response data.
+
+    Returns:
+        Optional[datetime]: The expiration or ``None`` if no expiration was
+            specified.
+    """
+    expires_in = response_data.get("expires_in", None)
+
+    if expires_in is not None:
+        return _helpers.utcnow() + datetime.timedelta(seconds=expires_in)
+    else:
+        return None
+
+
+def _token_endpoint_request_no_throw(
+    request, token_uri, body, access_token=None, use_json=False
+):
+    """Makes a request to the OAuth 2.0 authorization server's token endpoint.
+    This function doesn't throw on response errors.
+
+    Args:
+        request (google.auth.transport.Request): A callable used to make
+            HTTP requests.
+        token_uri (str): The OAuth 2.0 authorizations server's token endpoint
+            URI.
+        body (Mapping[str, str]): The parameters to send in the request body.
+        access_token (Optional(str)): The access token needed to make the request.
+        use_json (Optional(bool)): Use urlencoded format or json format for the
+            content type. The default value is False.
+
+    Returns:
+        Tuple(bool, Mapping[str, str]): A boolean indicating if the request is
+            successful, and a mapping for the JSON-decoded response data.
+    """
+    if use_json:
+        headers = {"Content-Type": _JSON_CONTENT_TYPE}
+        body = json.dumps(body).encode("utf-8")
+    else:
+        headers = {"Content-Type": _URLENCODED_CONTENT_TYPE}
+        body = urllib.parse.urlencode(body).encode("utf-8")
+
+    if access_token:
+        headers["Authorization"] = "Bearer {}".format(access_token)
+
+    retry = 0
+    # retry to fetch token for maximum of two times if any internal failure
+    # occurs.
+    while True:
+        response = request(method="POST", url=token_uri, headers=headers, body=body)
+        response_body = (
+            response.data.decode("utf-8")
+            if hasattr(response.data, "decode")
+            else response.data
+        )
+        response_data = json.loads(response_body)
+
+        if response.status == http_client.OK:
+            break
+        else:
+            error_desc = response_data.get("error_description") or ""
+            error_code = response_data.get("error") or ""
+            if (
+                any(e == "internal_failure" for e in (error_code, error_desc))
+                and retry < 1
+            ):
+                retry += 1
+                continue
+            return response.status == http_client.OK, response_data
+
+    return response.status == http_client.OK, response_data
+
+
+def _token_endpoint_request(
+    request, token_uri, body, access_token=None, use_json=False
+):
+    """Makes a request to the OAuth 2.0 authorization server's token endpoint.
+
+    Args:
+        request (google.auth.transport.Request): A callable used to make
+            HTTP requests.
+        token_uri (str): The OAuth 2.0 authorizations server's token endpoint
+            URI.
+        body (Mapping[str, str]): The parameters to send in the request body.
+        access_token (Optional(str)): The access token needed to make the request.
+        use_json (Optional(bool)): Use urlencoded format or json format for the
+            content type. The default value is False.
+
+    Returns:
+        Mapping[str, str]: The JSON-decoded response data.
+
+    Raises:
+        google.auth.exceptions.RefreshError: If the token endpoint returned
+            an error.
+    """
+    response_status_ok, response_data = _token_endpoint_request_no_throw(
+        request, token_uri, body, access_token=access_token, use_json=use_json
+    )
+    if not response_status_ok:
+        _handle_error_response(response_data)
+    return response_data
+
+
+def jwt_grant(request, token_uri, assertion):
+    """Implements the JWT Profile for OAuth 2.0 Authorization Grants.
+
+    For more details, see `rfc7523 section 4`_.
+
+    Args:
+        request (google.auth.transport.Request): A callable used to make
+            HTTP requests.
+        token_uri (str): The OAuth 2.0 authorizations server's token endpoint
+            URI.
+        assertion (str): The OAuth 2.0 assertion.
+
+    Returns:
+        Tuple[str, Optional[datetime], Mapping[str, str]]: The access token,
+            expiration, and additional data returned by the token endpoint.
+
+    Raises:
+        google.auth.exceptions.RefreshError: If the token endpoint returned
+            an error.
+
+    .. _rfc7523 section 4: https://tools.ietf.org/html/rfc7523#section-4
+    """
+    body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE}
+
+    response_data = _token_endpoint_request(request, token_uri, body)
+
+    try:
+        access_token = response_data["access_token"]
+    except KeyError as caught_exc:
+        new_exc = exceptions.RefreshError("No access token in response.", response_data)
+        six.raise_from(new_exc, caught_exc)
+
+    expiry = _parse_expiry(response_data)
+
+    return access_token, expiry, response_data
+
+
+def id_token_jwt_grant(request, token_uri, assertion):
+    """Implements the JWT Profile for OAuth 2.0 Authorization Grants, but
+    requests an OpenID Connect ID Token instead of an access token.
+
+    This is a variant on the standard JWT Profile that is currently unique
+    to Google. This was added for the benefit of authenticating to services
+    that require ID Tokens instead of access tokens or JWT bearer tokens.
+
+    Args:
+        request (google.auth.transport.Request): A callable used to make
+            HTTP requests.
+        token_uri (str): The OAuth 2.0 authorization server's token endpoint
+            URI.
+        assertion (str): JWT token signed by a service account. The token's
+            payload must include a ``target_audience`` claim.
+
+    Returns:
+        Tuple[str, Optional[datetime], Mapping[str, str]]:
+            The (encoded) Open ID Connect ID Token, expiration, and additional
+            data returned by the endpoint.
+
+    Raises:
+        google.auth.exceptions.RefreshError: If the token endpoint returned
+            an error.
+    """
+    body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE}
+
+    response_data = _token_endpoint_request(request, token_uri, body)
+
+    try:
+        id_token = response_data["id_token"]
+    except KeyError as caught_exc:
+        new_exc = exceptions.RefreshError("No ID token in response.", response_data)
+        six.raise_from(new_exc, caught_exc)
+
+    payload = jwt.decode(id_token, verify=False)
+    expiry = datetime.datetime.utcfromtimestamp(payload["exp"])
+
+    return id_token, expiry, response_data
+
+
+def _handle_refresh_grant_response(response_data, refresh_token):
+    """Extract tokens from refresh grant response.
+
+    Args:
+        response_data (Mapping[str, str]): Refresh grant response data.
+        refresh_token (str): Current refresh token.
+
+    Returns:
+        Tuple[str, str, Optional[datetime], Mapping[str, str]]: The access token,
+            refresh token, expiration, and additional data returned by the token
+            endpoint. If response_data doesn't have refresh token, then the current
+            refresh token will be returned.
+
+    Raises:
+        google.auth.exceptions.RefreshError: If the token endpoint returned
+            an error.
+    """
+    try:
+        access_token = response_data["access_token"]
+    except KeyError as caught_exc:
+        new_exc = exceptions.RefreshError("No access token in response.", response_data)
+        six.raise_from(new_exc, caught_exc)
+
+    refresh_token = response_data.get("refresh_token", refresh_token)
+    expiry = _parse_expiry(response_data)
+
+    return access_token, refresh_token, expiry, response_data
+
+
+def refresh_grant(
+    request,
+    token_uri,
+    refresh_token,
+    client_id,
+    client_secret,
+    scopes=None,
+    rapt_token=None,
+):
+    """Implements the OAuth 2.0 refresh token grant.
+
+    For more details, see `rfc678 section 6`_.
+
+    Args:
+        request (google.auth.transport.Request): A callable used to make
+            HTTP requests.
+        token_uri (str): The OAuth 2.0 authorizations server's token endpoint
+            URI.
+        refresh_token (str): The refresh token to use to get a new access
+            token.
+        client_id (str): The OAuth 2.0 application's client ID.
+        client_secret (str): The Oauth 2.0 appliaction's client secret.
+        scopes (Optional(Sequence[str])): Scopes to request. If present, all
+            scopes must be authorized for the refresh token. Useful if refresh
+            token has a wild card scope (e.g.
+            'https://www.googleapis.com/auth/any-api').
+        rapt_token (Optional(str)): The reauth Proof Token.
+
+    Returns:
+        Tuple[str, str, Optional[datetime], Mapping[str, str]]: The access
+            token, new or current refresh token, expiration, and additional data
+            returned by the token endpoint.
+
+    Raises:
+        google.auth.exceptions.RefreshError: If the token endpoint returned
+            an error.
+
+    .. _rfc6748 section 6: https://tools.ietf.org/html/rfc6749#section-6
+    """
+    body = {
+        "grant_type": _REFRESH_GRANT_TYPE,
+        "client_id": client_id,
+        "client_secret": client_secret,
+        "refresh_token": refresh_token,
+    }
+    if scopes:
+        body["scope"] = " ".join(scopes)
+    if rapt_token:
+        body["rapt"] = rapt_token
+
+    response_data = _token_endpoint_request(request, token_uri, body)
+    return _handle_refresh_grant_response(response_data, refresh_token)
diff --git a/google/oauth2/_client_async.py b/google/oauth2/_client_async.py
new file mode 100644
index 0000000..cf51211
--- /dev/null
+++ b/google/oauth2/_client_async.py
@@ -0,0 +1,263 @@
+# 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.
+
+"""OAuth 2.0 async client.
+
+This is a client for interacting with an OAuth 2.0 authorization server's
+token endpoint.
+
+For more information about the token endpoint, see
+`Section 3.1 of rfc6749`_
+
+.. _Section 3.1 of rfc6749: https://tools.ietf.org/html/rfc6749#section-3.2
+"""
+
+import datetime
+import json
+
+import six
+from six.moves import http_client
+from six.moves import urllib
+
+from google.auth import exceptions
+from google.auth import jwt
+from google.oauth2 import _client as client
+
+
+async def _token_endpoint_request_no_throw(
+    request, token_uri, body, access_token=None, use_json=False
+):
+    """Makes a request to the OAuth 2.0 authorization server's token endpoint.
+    This function doesn't throw on response errors.
+
+    Args:
+        request (google.auth.transport.Request): A callable used to make
+            HTTP requests.
+        token_uri (str): The OAuth 2.0 authorizations server's token endpoint
+            URI.
+        body (Mapping[str, str]): The parameters to send in the request body.
+        access_token (Optional(str)): The access token needed to make the request.
+        use_json (Optional(bool)): Use urlencoded format or json format for the
+            content type. The default value is False.
+
+    Returns:
+        Tuple(bool, Mapping[str, str]): A boolean indicating if the request is
+            successful, and a mapping for the JSON-decoded response data.
+    """
+    if use_json:
+        headers = {"Content-Type": client._JSON_CONTENT_TYPE}
+        body = json.dumps(body).encode("utf-8")
+    else:
+        headers = {"Content-Type": client._URLENCODED_CONTENT_TYPE}
+        body = urllib.parse.urlencode(body).encode("utf-8")
+
+    if access_token:
+        headers["Authorization"] = "Bearer {}".format(access_token)
+
+    retry = 0
+    # retry to fetch token for maximum of two times if any internal failure
+    # occurs.
+    while True:
+
+        response = await request(
+            method="POST", url=token_uri, headers=headers, body=body
+        )
+
+        # Using data.read() resulted in zlib decompression errors. This may require future investigation.
+        response_body1 = await response.content()
+
+        response_body = (
+            response_body1.decode("utf-8")
+            if hasattr(response_body1, "decode")
+            else response_body1
+        )
+
+        response_data = json.loads(response_body)
+
+        if response.status == http_client.OK:
+            break
+        else:
+            error_desc = response_data.get("error_description") or ""
+            error_code = response_data.get("error") or ""
+            if (
+                any(e == "internal_failure" for e in (error_code, error_desc))
+                and retry < 1
+            ):
+                retry += 1
+                continue
+            return response.status == http_client.OK, response_data
+
+    return response.status == http_client.OK, response_data
+
+
+async def _token_endpoint_request(
+    request, token_uri, body, access_token=None, use_json=False
+):
+    """Makes a request to the OAuth 2.0 authorization server's token endpoint.
+
+    Args:
+        request (google.auth.transport.Request): A callable used to make
+            HTTP requests.
+        token_uri (str): The OAuth 2.0 authorizations server's token endpoint
+            URI.
+        body (Mapping[str, str]): The parameters to send in the request body.
+        access_token (Optional(str)): The access token needed to make the request.
+        use_json (Optional(bool)): Use urlencoded format or json format for the
+            content type. The default value is False.
+
+    Returns:
+        Mapping[str, str]: The JSON-decoded response data.
+
+    Raises:
+        google.auth.exceptions.RefreshError: If the token endpoint returned
+            an error.
+    """
+    response_status_ok, response_data = await _token_endpoint_request_no_throw(
+        request, token_uri, body, access_token=access_token, use_json=use_json
+    )
+    if not response_status_ok:
+        client._handle_error_response(response_data)
+    return response_data
+
+
+async def jwt_grant(request, token_uri, assertion):
+    """Implements the JWT Profile for OAuth 2.0 Authorization Grants.
+
+    For more details, see `rfc7523 section 4`_.
+
+    Args:
+        request (google.auth.transport.Request): A callable used to make
+            HTTP requests.
+        token_uri (str): The OAuth 2.0 authorizations server's token endpoint
+            URI.
+        assertion (str): The OAuth 2.0 assertion.
+
+    Returns:
+        Tuple[str, Optional[datetime], Mapping[str, str]]: The access token,
+            expiration, and additional data returned by the token endpoint.
+
+    Raises:
+        google.auth.exceptions.RefreshError: If the token endpoint returned
+            an error.
+
+    .. _rfc7523 section 4: https://tools.ietf.org/html/rfc7523#section-4
+    """
+    body = {"assertion": assertion, "grant_type": client._JWT_GRANT_TYPE}
+
+    response_data = await _token_endpoint_request(request, token_uri, body)
+
+    try:
+        access_token = response_data["access_token"]
+    except KeyError as caught_exc:
+        new_exc = exceptions.RefreshError("No access token in response.", response_data)
+        six.raise_from(new_exc, caught_exc)
+
+    expiry = client._parse_expiry(response_data)
+
+    return access_token, expiry, response_data
+
+
+async def id_token_jwt_grant(request, token_uri, assertion):
+    """Implements the JWT Profile for OAuth 2.0 Authorization Grants, but
+    requests an OpenID Connect ID Token instead of an access token.
+
+    This is a variant on the standard JWT Profile that is currently unique
+    to Google. This was added for the benefit of authenticating to services
+    that require ID Tokens instead of access tokens or JWT bearer tokens.
+
+    Args:
+        request (google.auth.transport.Request): A callable used to make
+            HTTP requests.
+        token_uri (str): The OAuth 2.0 authorization server's token endpoint
+            URI.
+        assertion (str): JWT token signed by a service account. The token's
+            payload must include a ``target_audience`` claim.
+
+    Returns:
+        Tuple[str, Optional[datetime], Mapping[str, str]]:
+            The (encoded) Open ID Connect ID Token, expiration, and additional
+            data returned by the endpoint.
+
+    Raises:
+        google.auth.exceptions.RefreshError: If the token endpoint returned
+            an error.
+    """
+    body = {"assertion": assertion, "grant_type": client._JWT_GRANT_TYPE}
+
+    response_data = await _token_endpoint_request(request, token_uri, body)
+
+    try:
+        id_token = response_data["id_token"]
+    except KeyError as caught_exc:
+        new_exc = exceptions.RefreshError("No ID token in response.", response_data)
+        six.raise_from(new_exc, caught_exc)
+
+    payload = jwt.decode(id_token, verify=False)
+    expiry = datetime.datetime.utcfromtimestamp(payload["exp"])
+
+    return id_token, expiry, response_data
+
+
+async def refresh_grant(
+    request,
+    token_uri,
+    refresh_token,
+    client_id,
+    client_secret,
+    scopes=None,
+    rapt_token=None,
+):
+    """Implements the OAuth 2.0 refresh token grant.
+
+    For more details, see `rfc678 section 6`_.
+
+    Args:
+        request (google.auth.transport.Request): A callable used to make
+            HTTP requests.
+        token_uri (str): The OAuth 2.0 authorizations server's token endpoint
+            URI.
+        refresh_token (str): The refresh token to use to get a new access
+            token.
+        client_id (str): The OAuth 2.0 application's client ID.
+        client_secret (str): The Oauth 2.0 appliaction's client secret.
+        scopes (Optional(Sequence[str])): Scopes to request. If present, all
+            scopes must be authorized for the refresh token. Useful if refresh
+            token has a wild card scope (e.g.
+            'https://www.googleapis.com/auth/any-api').
+        rapt_token (Optional(str)): The reauth Proof Token.
+
+    Returns:
+        Tuple[str, Optional[str], Optional[datetime], Mapping[str, str]]: The
+            access token, new or current refresh token, expiration, and additional data
+            returned by the token endpoint.
+
+    Raises:
+        google.auth.exceptions.RefreshError: If the token endpoint returned
+            an error.
+
+    .. _rfc6748 section 6: https://tools.ietf.org/html/rfc6749#section-6
+    """
+    body = {
+        "grant_type": client._REFRESH_GRANT_TYPE,
+        "client_id": client_id,
+        "client_secret": client_secret,
+        "refresh_token": refresh_token,
+    }
+    if scopes:
+        body["scope"] = " ".join(scopes)
+    if rapt_token:
+        body["rapt"] = rapt_token
+
+    response_data = await _token_endpoint_request(request, token_uri, body)
+    return client._handle_refresh_grant_response(response_data, refresh_token)
diff --git a/google/oauth2/_credentials_async.py b/google/oauth2/_credentials_async.py
new file mode 100644
index 0000000..e7b9637
--- /dev/null
+++ b/google/oauth2/_credentials_async.py
@@ -0,0 +1,112 @@
+# 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.
+
+"""OAuth 2.0 Async Credentials.
+
+This module provides credentials based on OAuth 2.0 access and refresh tokens.
+These credentials usually access resources on behalf of a user (resource
+owner).
+
+Specifically, this is intended to use access tokens acquired using the
+`Authorization Code grant`_ and can refresh those tokens using a
+optional `refresh token`_.
+
+Obtaining the initial access and refresh token is outside of the scope of this
+module. Consult `rfc6749 section 4.1`_ for complete details on the
+Authorization Code grant flow.
+
+.. _Authorization Code grant: https://tools.ietf.org/html/rfc6749#section-1.3.1
+.. _refresh token: https://tools.ietf.org/html/rfc6749#section-6
+.. _rfc6749 section 4.1: https://tools.ietf.org/html/rfc6749#section-4.1
+"""
+
+from google.auth import _credentials_async as credentials
+from google.auth import _helpers
+from google.auth import exceptions
+from google.oauth2 import _reauth_async as reauth
+from google.oauth2 import credentials as oauth2_credentials
+
+
+class Credentials(oauth2_credentials.Credentials):
+    """Credentials using OAuth 2.0 access and refresh tokens.
+
+    The credentials are considered immutable. If you want to modify the
+    quota project, use :meth:`with_quota_project` or ::
+
+        credentials = credentials.with_quota_project('myproject-123)
+    """
+
+    @_helpers.copy_docstring(credentials.Credentials)
+    async def refresh(self, request):
+        if (
+            self._refresh_token is None
+            or self._token_uri is None
+            or self._client_id is None
+            or self._client_secret is None
+        ):
+            raise exceptions.RefreshError(
+                "The credentials do not contain the necessary fields need to "
+                "refresh the access token. You must specify refresh_token, "
+                "token_uri, client_id, and client_secret."
+            )
+
+        (
+            access_token,
+            refresh_token,
+            expiry,
+            grant_response,
+            rapt_token,
+        ) = await reauth.refresh_grant(
+            request,
+            self._token_uri,
+            self._refresh_token,
+            self._client_id,
+            self._client_secret,
+            scopes=self._scopes,
+            rapt_token=self._rapt_token,
+            enable_reauth_refresh=self._enable_reauth_refresh,
+        )
+
+        self.token = access_token
+        self.expiry = expiry
+        self._refresh_token = refresh_token
+        self._id_token = grant_response.get("id_token")
+        self._rapt_token = rapt_token
+
+        if self._scopes and "scope" in grant_response:
+            requested_scopes = frozenset(self._scopes)
+            granted_scopes = frozenset(grant_response["scope"].split())
+            scopes_requested_but_not_granted = requested_scopes - granted_scopes
+            if scopes_requested_but_not_granted:
+                raise exceptions.RefreshError(
+                    "Not all requested scopes were granted by the "
+                    "authorization server, missing scopes {}.".format(
+                        ", ".join(scopes_requested_but_not_granted)
+                    )
+                )
+
+
+class UserAccessTokenCredentials(oauth2_credentials.UserAccessTokenCredentials):
+    """Access token credentials for user account.
+
+    Obtain the access token for a given user account or the current active
+    user account with the ``gcloud auth print-access-token`` command.
+
+    Args:
+        account (Optional[str]): Account to get the access token for. If not
+            specified, the current active account will be used.
+        quota_project_id (Optional[str]): The project ID used for quota
+            and billing.
+
+    """
diff --git a/google/oauth2/_id_token_async.py b/google/oauth2/_id_token_async.py
new file mode 100644
index 0000000..20630e0
--- /dev/null
+++ b/google/oauth2/_id_token_async.py
@@ -0,0 +1,287 @@
+# 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.
+
+"""Google ID Token helpers.
+
+Provides support for verifying `OpenID Connect ID Tokens`_, especially ones
+generated by Google infrastructure.
+
+To parse and verify an ID Token issued by Google's OAuth 2.0 authorization
+server use :func:`verify_oauth2_token`. To verify an ID Token issued by
+Firebase, use :func:`verify_firebase_token`.
+
+A general purpose ID Token verifier is available as :func:`verify_token`.
+
+Example::
+
+    from google.oauth2 import _id_token_async
+    from google.auth.transport import aiohttp_requests
+
+    request = aiohttp_requests.Request()
+
+    id_info = await _id_token_async.verify_oauth2_token(
+        token, request, 'my-client-id.example.com')
+
+    if id_info['iss'] != 'https://accounts.google.com':
+        raise ValueError('Wrong issuer.')
+
+    userid = id_info['sub']
+
+By default, this will re-fetch certificates for each verification. Because
+Google's public keys are only changed infrequently (on the order of once per
+day), you may wish to take advantage of caching to reduce latency and the
+potential for network errors. This can be accomplished using an external
+library like `CacheControl`_ to create a cache-aware
+:class:`google.auth.transport.Request`::
+
+    import cachecontrol
+    import google.auth.transport.requests
+    import requests
+
+    session = requests.session()
+    cached_session = cachecontrol.CacheControl(session)
+    request = google.auth.transport.requests.Request(session=cached_session)
+
+.. _OpenID Connect ID Token:
+    http://openid.net/specs/openid-connect-core-1_0.html#IDToken
+.. _CacheControl: https://cachecontrol.readthedocs.io
+"""
+
+import json
+import os
+
+import six
+from six.moves import http_client
+
+from google.auth import environment_vars
+from google.auth import exceptions
+from google.auth import jwt
+from google.auth.transport import requests
+from google.oauth2 import id_token as sync_id_token
+
+
+async def _fetch_certs(request, certs_url):
+    """Fetches certificates.
+
+    Google-style cerificate endpoints return JSON in the format of
+    ``{'key id': 'x509 certificate'}``.
+
+    Args:
+        request (google.auth.transport.Request): The object used to make
+            HTTP requests. This must be an aiohttp request.
+        certs_url (str): The certificate endpoint URL.
+
+    Returns:
+        Mapping[str, str]: A mapping of public key ID to x.509 certificate
+            data.
+    """
+    response = await request(certs_url, method="GET")
+
+    if response.status != http_client.OK:
+        raise exceptions.TransportError(
+            "Could not fetch certificates at {}".format(certs_url)
+        )
+
+    data = await response.data.read()
+
+    return json.loads(json.dumps(data))
+
+
+async def verify_token(
+    id_token,
+    request,
+    audience=None,
+    certs_url=sync_id_token._GOOGLE_OAUTH2_CERTS_URL,
+    clock_skew_in_seconds=0,
+):
+    """Verifies an ID token and returns the decoded token.
+
+    Args:
+        id_token (Union[str, bytes]): The encoded token.
+        request (google.auth.transport.Request): The object used to make
+            HTTP requests. This must be an aiohttp request.
+        audience (str): The audience that this token is intended for. If None
+            then the audience is not verified.
+        certs_url (str): The URL that specifies the certificates to use to
+            verify the token. This URL should return JSON in the format of
+            ``{'key id': 'x509 certificate'}``.
+        clock_skew_in_seconds (int): The clock skew used for `iat` and `exp`
+            validation.
+
+    Returns:
+        Mapping[str, Any]: The decoded token.
+    """
+    certs = await _fetch_certs(request, certs_url)
+
+    return jwt.decode(
+        id_token,
+        certs=certs,
+        audience=audience,
+        clock_skew_in_seconds=clock_skew_in_seconds,
+    )
+
+
+async def verify_oauth2_token(
+    id_token, request, audience=None, clock_skew_in_seconds=0
+):
+    """Verifies an ID Token issued by Google's OAuth 2.0 authorization server.
+
+    Args:
+        id_token (Union[str, bytes]): The encoded token.
+        request (google.auth.transport.Request): The object used to make
+            HTTP requests. This must be an aiohttp request.
+        audience (str): The audience that this token is intended for. This is
+            typically your application's OAuth 2.0 client ID. If None then the
+            audience is not verified.
+        clock_skew_in_seconds (int): The clock skew used for `iat` and `exp`
+            validation.
+
+    Returns:
+        Mapping[str, Any]: The decoded token.
+
+    Raises:
+        exceptions.GoogleAuthError: If the issuer is invalid.
+    """
+    idinfo = await verify_token(
+        id_token,
+        request,
+        audience=audience,
+        certs_url=sync_id_token._GOOGLE_OAUTH2_CERTS_URL,
+        clock_skew_in_seconds=clock_skew_in_seconds,
+    )
+
+    if idinfo["iss"] not in sync_id_token._GOOGLE_ISSUERS:
+        raise exceptions.GoogleAuthError(
+            "Wrong issuer. 'iss' should be one of the following: {}".format(
+                sync_id_token._GOOGLE_ISSUERS
+            )
+        )
+
+    return idinfo
+
+
+async def verify_firebase_token(
+    id_token, request, audience=None, clock_skew_in_seconds=0
+):
+    """Verifies an ID Token issued by Firebase Authentication.
+
+    Args:
+        id_token (Union[str, bytes]): The encoded token.
+        request (google.auth.transport.Request): The object used to make
+            HTTP requests. This must be an aiohttp request.
+        audience (str): The audience that this token is intended for. This is
+            typically your Firebase application ID. If None then the audience
+            is not verified.
+        clock_skew_in_seconds (int): The clock skew used for `iat` and `exp`
+            validation.
+
+    Returns:
+        Mapping[str, Any]: The decoded token.
+    """
+    return await verify_token(
+        id_token,
+        request,
+        audience=audience,
+        certs_url=sync_id_token._GOOGLE_APIS_CERTS_URL,
+        clock_skew_in_seconds=clock_skew_in_seconds,
+    )
+
+
+async def fetch_id_token(request, audience):
+    """Fetch the ID Token from the current environment.
+
+    This function acquires ID token from the environment in the following order.
+    See https://google.aip.dev/auth/4110.
+
+    1. If the environment variable ``GOOGLE_APPLICATION_CREDENTIALS`` is set
+       to the path of a valid service account JSON file, then ID token is
+       acquired using this service account credentials.
+    2. If the application is running in Compute Engine, App Engine or Cloud Run,
+       then the ID token are obtained from the metadata server.
+    3. If metadata server doesn't exist and no valid service account credentials
+       are found, :class:`~google.auth.exceptions.DefaultCredentialsError` will
+       be raised.
+
+    Example::
+
+        import google.oauth2._id_token_async
+        import google.auth.transport.aiohttp_requests
+
+        request = google.auth.transport.aiohttp_requests.Request()
+        target_audience = "https://pubsub.googleapis.com"
+
+        id_token = await google.oauth2._id_token_async.fetch_id_token(request, target_audience)
+
+    Args:
+        request (google.auth.transport.aiohttp_requests.Request): A callable used to make
+            HTTP requests.
+        audience (str): The audience that this ID token is intended for.
+
+    Returns:
+        str: The ID token.
+
+    Raises:
+        ~google.auth.exceptions.DefaultCredentialsError:
+            If metadata server doesn't exist and no valid service account
+            credentials are found.
+    """
+    # 1. Try to get credentials from the GOOGLE_APPLICATION_CREDENTIALS environment
+    # variable.
+    credentials_filename = os.environ.get(environment_vars.CREDENTIALS)
+    if credentials_filename:
+        if not (
+            os.path.exists(credentials_filename)
+            and os.path.isfile(credentials_filename)
+        ):
+            raise exceptions.DefaultCredentialsError(
+                "GOOGLE_APPLICATION_CREDENTIALS path is either not found or invalid."
+            )
+
+        try:
+            with open(credentials_filename, "r") as f:
+                from google.oauth2 import _service_account_async as service_account
+
+                info = json.load(f)
+                if info.get("type") == "service_account":
+                    credentials = service_account.IDTokenCredentials.from_service_account_info(
+                        info, target_audience=audience
+                    )
+                    await credentials.refresh(request)
+                    return credentials.token
+        except ValueError as caught_exc:
+            new_exc = exceptions.DefaultCredentialsError(
+                "GOOGLE_APPLICATION_CREDENTIALS is not valid service account credentials.",
+                caught_exc,
+            )
+            six.raise_from(new_exc, caught_exc)
+
+    # 2. Try to fetch ID token from metada server if it exists. The code works
+    # for GAE and Cloud Run metadata server as well.
+    try:
+        from google.auth import compute_engine
+        from google.auth.compute_engine import _metadata
+
+        request_new = requests.Request()
+        if _metadata.ping(request_new):
+            credentials = compute_engine.IDTokenCredentials(
+                request_new, audience, use_metadata_identity_endpoint=True
+            )
+            credentials.refresh(request_new)
+            return credentials.token
+    except (ImportError, exceptions.TransportError):
+        pass
+
+    raise exceptions.DefaultCredentialsError(
+        "Neither metadata server or valid service account credentials are found."
+    )
diff --git a/google/oauth2/_reauth_async.py b/google/oauth2/_reauth_async.py
new file mode 100644
index 0000000..0276ddd
--- /dev/null
+++ b/google/oauth2/_reauth_async.py
@@ -0,0 +1,329 @@
+# 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.
+
+"""A module that provides functions for handling rapt authentication.
+
+Reauth is a process of obtaining additional authentication (such as password,
+security token, etc.) while refreshing OAuth 2.0 credentials for a user.
+
+Credentials that use the Reauth flow must have the reauth scope,
+``https://www.googleapis.com/auth/accounts.reauth``.
+
+This module provides a high-level function for executing the Reauth process,
+:func:`refresh_grant`, and lower-level helpers for doing the individual
+steps of the reauth process.
+
+Those steps are:
+
+1. Obtaining a list of challenges from the reauth server.
+2. Running through each challenge and sending the result back to the reauth
+   server.
+3. Refreshing the access token using the returned rapt token.
+"""
+
+import sys
+
+from six.moves import range
+
+from google.auth import exceptions
+from google.oauth2 import _client
+from google.oauth2 import _client_async
+from google.oauth2 import challenges
+from google.oauth2 import reauth
+
+
+async def _get_challenges(
+    request, supported_challenge_types, access_token, requested_scopes=None
+):
+    """Does initial request to reauth API to get the challenges.
+
+    Args:
+        request (google.auth.transport.Request): A callable used to make
+            HTTP requests. This must be an aiohttp request.
+        supported_challenge_types (Sequence[str]): list of challenge names
+            supported by the manager.
+        access_token (str): Access token with reauth scopes.
+        requested_scopes (Optional(Sequence[str])): Authorized scopes for the credentials.
+
+    Returns:
+        dict: The response from the reauth API.
+    """
+    body = {"supportedChallengeTypes": supported_challenge_types}
+    if requested_scopes:
+        body["oauthScopesForDomainPolicyLookup"] = requested_scopes
+
+    return await _client_async._token_endpoint_request(
+        request,
+        reauth._REAUTH_API + ":start",
+        body,
+        access_token=access_token,
+        use_json=True,
+    )
+
+
+async def _send_challenge_result(
+    request, session_id, challenge_id, client_input, access_token
+):
+    """Attempt to refresh access token by sending next challenge result.
+
+    Args:
+        request (google.auth.transport.Request): A callable used to make
+            HTTP requests. This must be an aiohttp request.
+        session_id (str): session id returned by the initial reauth call.
+        challenge_id (str): challenge id returned by the initial reauth call.
+        client_input: dict with a challenge-specific client input. For example:
+            ``{'credential': password}`` for password challenge.
+        access_token (str): Access token with reauth scopes.
+
+    Returns:
+        dict: The response from the reauth API.
+    """
+    body = {
+        "sessionId": session_id,
+        "challengeId": challenge_id,
+        "action": "RESPOND",
+        "proposalResponse": client_input,
+    }
+
+    return await _client_async._token_endpoint_request(
+        request,
+        reauth._REAUTH_API + "/{}:continue".format(session_id),
+        body,
+        access_token=access_token,
+        use_json=True,
+    )
+
+
+async def _run_next_challenge(msg, request, access_token):
+    """Get the next challenge from msg and run it.
+
+    Args:
+        msg (dict): Reauth API response body (either from the initial request to
+            https://reauth.googleapis.com/v2/sessions:start or from sending the
+            previous challenge response to
+            https://reauth.googleapis.com/v2/sessions/id:continue)
+        request (google.auth.transport.Request): A callable used to make
+            HTTP requests. This must be an aiohttp request.
+        access_token (str): reauth access token
+
+    Returns:
+        dict: The response from the reauth API.
+
+    Raises:
+        google.auth.exceptions.ReauthError: if reauth failed.
+    """
+    for challenge in msg["challenges"]:
+        if challenge["status"] != "READY":
+            # Skip non-activated challenges.
+            continue
+        c = challenges.AVAILABLE_CHALLENGES.get(challenge["challengeType"], None)
+        if not c:
+            raise exceptions.ReauthFailError(
+                "Unsupported challenge type {0}. Supported types: {1}".format(
+                    challenge["challengeType"],
+                    ",".join(list(challenges.AVAILABLE_CHALLENGES.keys())),
+                )
+            )
+        if not c.is_locally_eligible:
+            raise exceptions.ReauthFailError(
+                "Challenge {0} is not locally eligible".format(
+                    challenge["challengeType"]
+                )
+            )
+        client_input = c.obtain_challenge_input(challenge)
+        if not client_input:
+            return None
+        return await _send_challenge_result(
+            request,
+            msg["sessionId"],
+            challenge["challengeId"],
+            client_input,
+            access_token,
+        )
+    return None
+
+
+async def _obtain_rapt(request, access_token, requested_scopes):
+    """Given an http request method and reauth access token, get rapt token.
+
+    Args:
+        request (google.auth.transport.Request): A callable used to make
+            HTTP requests. This must be an aiohttp request.
+        access_token (str): reauth access token
+        requested_scopes (Sequence[str]): scopes required by the client application
+
+    Returns:
+        str: The rapt token.
+
+    Raises:
+        google.auth.exceptions.ReauthError: if reauth failed
+    """
+    msg = await _get_challenges(
+        request,
+        list(challenges.AVAILABLE_CHALLENGES.keys()),
+        access_token,
+        requested_scopes,
+    )
+
+    if msg["status"] == reauth._AUTHENTICATED:
+        return msg["encodedProofOfReauthToken"]
+
+    for _ in range(0, reauth.RUN_CHALLENGE_RETRY_LIMIT):
+        if not (
+            msg["status"] == reauth._CHALLENGE_REQUIRED
+            or msg["status"] == reauth._CHALLENGE_PENDING
+        ):
+            raise exceptions.ReauthFailError(
+                "Reauthentication challenge failed due to API error: {}".format(
+                    msg["status"]
+                )
+            )
+
+        if not reauth.is_interactive():
+            raise exceptions.ReauthFailError(
+                "Reauthentication challenge could not be answered because you are not"
+                " in an interactive session."
+            )
+
+        msg = await _run_next_challenge(msg, request, access_token)
+
+        if msg["status"] == reauth._AUTHENTICATED:
+            return msg["encodedProofOfReauthToken"]
+
+    # If we got here it means we didn't get authenticated.
+    raise exceptions.ReauthFailError("Failed to obtain rapt token.")
+
+
+async def get_rapt_token(
+    request, client_id, client_secret, refresh_token, token_uri, scopes=None
+):
+    """Given an http request method and refresh_token, get rapt token.
+
+    Args:
+        request (google.auth.transport.Request): A callable used to make
+            HTTP requests. This must be an aiohttp request.
+        client_id (str): client id to get access token for reauth scope.
+        client_secret (str): client secret for the client_id
+        refresh_token (str): refresh token to refresh access token
+        token_uri (str): uri to refresh access token
+        scopes (Optional(Sequence[str])): scopes required by the client application
+
+    Returns:
+        str: The rapt token.
+    Raises:
+        google.auth.exceptions.RefreshError: If reauth failed.
+    """
+    sys.stderr.write("Reauthentication required.\n")
+
+    # Get access token for reauth.
+    access_token, _, _, _ = await _client_async.refresh_grant(
+        request=request,
+        client_id=client_id,
+        client_secret=client_secret,
+        refresh_token=refresh_token,
+        token_uri=token_uri,
+        scopes=[reauth._REAUTH_SCOPE],
+    )
+
+    # Get rapt token from reauth API.
+    rapt_token = await _obtain_rapt(request, access_token, requested_scopes=scopes)
+
+    return rapt_token
+
+
+async def refresh_grant(
+    request,
+    token_uri,
+    refresh_token,
+    client_id,
+    client_secret,
+    scopes=None,
+    rapt_token=None,
+    enable_reauth_refresh=False,
+):
+    """Implements the reauthentication flow.
+
+    Args:
+        request (google.auth.transport.Request): A callable used to make
+            HTTP requests. This must be an aiohttp request.
+        token_uri (str): The OAuth 2.0 authorizations server's token endpoint
+            URI.
+        refresh_token (str): The refresh token to use to get a new access
+            token.
+        client_id (str): The OAuth 2.0 application's client ID.
+        client_secret (str): The Oauth 2.0 appliaction's client secret.
+        scopes (Optional(Sequence[str])): Scopes to request. If present, all
+            scopes must be authorized for the refresh token. Useful if refresh
+            token has a wild card scope (e.g.
+            'https://www.googleapis.com/auth/any-api').
+        rapt_token (Optional(str)): The rapt token for reauth.
+        enable_reauth_refresh (Optional[bool]): Whether reauth refresh flow
+            should be used. The default value is False. This option is for
+            gcloud only, other users should use the default value.
+
+    Returns:
+        Tuple[str, Optional[str], Optional[datetime], Mapping[str, str], str]: The
+            access token, new refresh token, expiration, the additional data
+            returned by the token endpoint, and the rapt token.
+
+    Raises:
+        google.auth.exceptions.RefreshError: If the token endpoint returned
+            an error.
+    """
+    body = {
+        "grant_type": _client._REFRESH_GRANT_TYPE,
+        "client_id": client_id,
+        "client_secret": client_secret,
+        "refresh_token": refresh_token,
+    }
+    if scopes:
+        body["scope"] = " ".join(scopes)
+    if rapt_token:
+        body["rapt"] = rapt_token
+
+    response_status_ok, response_data = await _client_async._token_endpoint_request_no_throw(
+        request, token_uri, body
+    )
+    if (
+        not response_status_ok
+        and response_data.get("error") == reauth._REAUTH_NEEDED_ERROR
+        and (
+            response_data.get("error_subtype")
+            == reauth._REAUTH_NEEDED_ERROR_INVALID_RAPT
+            or response_data.get("error_subtype")
+            == reauth._REAUTH_NEEDED_ERROR_RAPT_REQUIRED
+        )
+    ):
+        if not enable_reauth_refresh:
+            raise exceptions.RefreshError(
+                "Reauthentication is needed. Please run `gcloud auth login --update-adc` to reauthenticate."
+            )
+
+        rapt_token = await get_rapt_token(
+            request, client_id, client_secret, refresh_token, token_uri, scopes=scopes
+        )
+        body["rapt"] = rapt_token
+        (
+            response_status_ok,
+            response_data,
+        ) = await _client_async._token_endpoint_request_no_throw(
+            request, token_uri, body
+        )
+
+    if not response_status_ok:
+        _client._handle_error_response(response_data)
+    refresh_response = _client._handle_refresh_grant_response(
+        response_data, refresh_token
+    )
+    return refresh_response + (rapt_token,)
diff --git a/google/oauth2/_service_account_async.py b/google/oauth2/_service_account_async.py
new file mode 100644
index 0000000..cfd315a
--- /dev/null
+++ b/google/oauth2/_service_account_async.py
@@ -0,0 +1,132 @@
+# 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.
+
+"""Service Accounts: JSON Web Token (JWT) Profile for OAuth 2.0
+
+NOTE: This file adds asynchronous refresh methods to both credentials
+classes, and therefore async/await syntax is required when calling this
+method when using service account credentials with asynchronous functionality.
+Otherwise, all other methods are inherited from the regular service account
+credentials file google.oauth2.service_account
+
+"""
+
+from google.auth import _credentials_async as credentials_async
+from google.auth import _helpers
+from google.oauth2 import _client_async
+from google.oauth2 import service_account
+
+
+class Credentials(
+    service_account.Credentials, credentials_async.Scoped, credentials_async.Credentials
+):
+    """Service account credentials
+
+    Usually, you'll create these credentials with one of the helper
+    constructors. To create credentials using a Google service account
+    private key JSON file::
+
+        credentials = _service_account_async.Credentials.from_service_account_file(
+            'service-account.json')
+
+    Or if you already have the service account file loaded::
+
+        service_account_info = json.load(open('service_account.json'))
+        credentials = _service_account_async.Credentials.from_service_account_info(
+            service_account_info)
+
+    Both helper methods pass on arguments to the constructor, so you can
+    specify additional scopes and a subject if necessary::
+
+        credentials = _service_account_async.Credentials.from_service_account_file(
+            'service-account.json',
+            scopes=['email'],
+            subject='[email protected]')
+
+    The credentials are considered immutable. If you want to modify the scopes
+    or the subject used for delegation, use :meth:`with_scopes` or
+    :meth:`with_subject`::
+
+        scoped_credentials = credentials.with_scopes(['email'])
+        delegated_credentials = credentials.with_subject(subject)
+
+    To add a quota project, use :meth:`with_quota_project`::
+
+        credentials = credentials.with_quota_project('myproject-123')
+    """
+
+    @_helpers.copy_docstring(credentials_async.Credentials)
+    async def refresh(self, request):
+        assertion = self._make_authorization_grant_assertion()
+        access_token, expiry, _ = await _client_async.jwt_grant(
+            request, self._token_uri, assertion
+        )
+        self.token = access_token
+        self.expiry = expiry
+
+
+class IDTokenCredentials(
+    service_account.IDTokenCredentials,
+    credentials_async.Signing,
+    credentials_async.Credentials,
+):
+    """Open ID Connect ID Token-based service account credentials.
+
+    These credentials are largely similar to :class:`.Credentials`, but instead
+    of using an OAuth 2.0 Access Token as the bearer token, they use an Open
+    ID Connect ID Token as the bearer token. These credentials are useful when
+    communicating to services that require ID Tokens and can not accept access
+    tokens.
+
+    Usually, you'll create these credentials with one of the helper
+    constructors. To create credentials using a Google service account
+    private key JSON file::
+
+        credentials = (
+            _service_account_async.IDTokenCredentials.from_service_account_file(
+                'service-account.json'))
+
+    Or if you already have the service account file loaded::
+
+        service_account_info = json.load(open('service_account.json'))
+        credentials = (
+            _service_account_async.IDTokenCredentials.from_service_account_info(
+                service_account_info))
+
+    Both helper methods pass on arguments to the constructor, so you can
+    specify additional scopes and a subject if necessary::
+
+        credentials = (
+            _service_account_async.IDTokenCredentials.from_service_account_file(
+                'service-account.json',
+                scopes=['email'],
+                subject='[email protected]'))
+
+    The credentials are considered immutable. If you want to modify the scopes
+    or the subject used for delegation, use :meth:`with_scopes` or
+    :meth:`with_subject`::
+
+        scoped_credentials = credentials.with_scopes(['email'])
+        delegated_credentials = credentials.with_subject(subject)
+
+    """
+
+    @_helpers.copy_docstring(credentials_async.Credentials)
+    async def refresh(self, request):
+        assertion = self._make_authorization_grant_assertion()
+        access_token, expiry, _ = await _client_async.id_token_jwt_grant(
+            request, self._token_uri, assertion
+        )
+        self.token = access_token
+        self.expiry = expiry
diff --git a/google/oauth2/challenges.py b/google/oauth2/challenges.py
new file mode 100644
index 0000000..95e76cb
--- /dev/null
+++ b/google/oauth2/challenges.py
@@ -0,0 +1,183 @@
+# 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.
+
+""" Challenges for reauthentication.
+"""
+
+import abc
+import base64
+import getpass
+import sys
+
+import six
+
+from google.auth import _helpers
+from google.auth import exceptions
+
+
+REAUTH_ORIGIN = "https://accounts.google.com"
+SAML_CHALLENGE_MESSAGE = (
+    "Please run `gcloud auth login` to complete reauthentication with SAML."
+)
+
+
+def get_user_password(text):
+    """Get password from user.
+
+    Override this function with a different logic if you are using this library
+    outside a CLI.
+
+    Args:
+        text (str): message for the password prompt.
+
+    Returns:
+        str: password string.
+    """
+    return getpass.getpass(text)
+
+
[email protected]_metaclass(abc.ABCMeta)
+class ReauthChallenge(object):
+    """Base class for reauth challenges."""
+
+    @property
+    @abc.abstractmethod
+    def name(self):  # pragma: NO COVER
+        """Returns the name of the challenge."""
+        raise NotImplementedError("name property must be implemented")
+
+    @property
+    @abc.abstractmethod
+    def is_locally_eligible(self):  # pragma: NO COVER
+        """Returns true if a challenge is supported locally on this machine."""
+        raise NotImplementedError("is_locally_eligible property must be implemented")
+
+    @abc.abstractmethod
+    def obtain_challenge_input(self, metadata):  # pragma: NO COVER
+        """Performs logic required to obtain credentials and returns it.
+
+        Args:
+            metadata (Mapping): challenge metadata returned in the 'challenges' field in
+                the initial reauth request. Includes the 'challengeType' field
+                and other challenge-specific fields.
+
+        Returns:
+            response that will be send to the reauth service as the content of
+            the 'proposalResponse' field in the request body. Usually a dict
+            with the keys specific to the challenge. For example,
+            ``{'credential': password}`` for password challenge.
+        """
+        raise NotImplementedError("obtain_challenge_input method must be implemented")
+
+
+class PasswordChallenge(ReauthChallenge):
+    """Challenge that asks for user's password."""
+
+    @property
+    def name(self):
+        return "PASSWORD"
+
+    @property
+    def is_locally_eligible(self):
+        return True
+
+    @_helpers.copy_docstring(ReauthChallenge)
+    def obtain_challenge_input(self, unused_metadata):
+        passwd = get_user_password("Please enter your password:")
+        if not passwd:
+            passwd = " "  # avoid the server crashing in case of no password :D
+        return {"credential": passwd}
+
+
+class SecurityKeyChallenge(ReauthChallenge):
+    """Challenge that asks for user's security key touch."""
+
+    @property
+    def name(self):
+        return "SECURITY_KEY"
+
+    @property
+    def is_locally_eligible(self):
+        return True
+
+    @_helpers.copy_docstring(ReauthChallenge)
+    def obtain_challenge_input(self, metadata):
+        try:
+            import pyu2f.convenience.authenticator
+            import pyu2f.errors
+            import pyu2f.model
+        except ImportError:
+            raise exceptions.ReauthFailError(
+                "pyu2f dependency is required to use Security key reauth feature. "
+                "It can be installed via `pip install pyu2f` or `pip install google-auth[reauth]`."
+            )
+        sk = metadata["securityKey"]
+        challenges = sk["challenges"]
+        app_id = sk["applicationId"]
+
+        challenge_data = []
+        for c in challenges:
+            kh = c["keyHandle"].encode("ascii")
+            key = pyu2f.model.RegisteredKey(bytearray(base64.urlsafe_b64decode(kh)))
+            challenge = c["challenge"].encode("ascii")
+            challenge = base64.urlsafe_b64decode(challenge)
+            challenge_data.append({"key": key, "challenge": challenge})
+
+        try:
+            api = pyu2f.convenience.authenticator.CreateCompositeAuthenticator(
+                REAUTH_ORIGIN
+            )
+            response = api.Authenticate(
+                app_id, challenge_data, print_callback=sys.stderr.write
+            )
+            return {"securityKey": response}
+        except pyu2f.errors.U2FError as e:
+            if e.code == pyu2f.errors.U2FError.DEVICE_INELIGIBLE:
+                sys.stderr.write("Ineligible security key.\n")
+            elif e.code == pyu2f.errors.U2FError.TIMEOUT:
+                sys.stderr.write("Timed out while waiting for security key touch.\n")
+            else:
+                raise e
+        except pyu2f.errors.NoDeviceFoundError:
+            sys.stderr.write("No security key found.\n")
+        return None
+
+
+class SamlChallenge(ReauthChallenge):
+    """Challenge that asks the users to browse to their ID Providers.
+
+    Currently SAML challenge is not supported. When obtaining the challenge
+    input, exception will be raised to instruct the users to run
+    `gcloud auth login` for reauthentication.
+    """
+
+    @property
+    def name(self):
+        return "SAML"
+
+    @property
+    def is_locally_eligible(self):
+        return True
+
+    def obtain_challenge_input(self, metadata):
+        # Magic Arch has not fully supported returning a proper dedirect URL
+        # for programmatic SAML users today. So we error our here and request
+        # users to use gcloud to complete a login.
+        raise exceptions.ReauthSamlChallengeFailError(SAML_CHALLENGE_MESSAGE)
+
+
+AVAILABLE_CHALLENGES = {
+    challenge.name: challenge
+    for challenge in [SecurityKeyChallenge(), PasswordChallenge(), SamlChallenge()]
+}
diff --git a/google/oauth2/credentials.py b/google/oauth2/credentials.py
new file mode 100644
index 0000000..9b59f8c
--- /dev/null
+++ b/google/oauth2/credentials.py
@@ -0,0 +1,490 @@
+# Copyright 2016 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.
+
+"""OAuth 2.0 Credentials.
+
+This module provides credentials based on OAuth 2.0 access and refresh tokens.
+These credentials usually access resources on behalf of a user (resource
+owner).
+
+Specifically, this is intended to use access tokens acquired using the
+`Authorization Code grant`_ and can refresh those tokens using a
+optional `refresh token`_.
+
+Obtaining the initial access and refresh token is outside of the scope of this
+module. Consult `rfc6749 section 4.1`_ for complete details on the
+Authorization Code grant flow.
+
+.. _Authorization Code grant: https://tools.ietf.org/html/rfc6749#section-1.3.1
+.. _refresh token: https://tools.ietf.org/html/rfc6749#section-6
+.. _rfc6749 section 4.1: https://tools.ietf.org/html/rfc6749#section-4.1
+"""
+
+from datetime import datetime
+import io
+import json
+
+import six
+
+from google.auth import _cloud_sdk
+from google.auth import _helpers
+from google.auth import credentials
+from google.auth import exceptions
+from google.oauth2 import reauth
+
+
+# The Google OAuth 2.0 token endpoint. Used for authorized user credentials.
+_GOOGLE_OAUTH2_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token"
+
+
+class Credentials(credentials.ReadOnlyScoped, credentials.CredentialsWithQuotaProject):
+    """Credentials using OAuth 2.0 access and refresh tokens.
+
+    The credentials are considered immutable. If you want to modify the
+    quota project, use :meth:`with_quota_project` or ::
+
+        credentials = credentials.with_quota_project('myproject-123)
+
+    Reauth is disabled by default. To enable reauth, set the
+    `enable_reauth_refresh` parameter to True in the constructor. Note that
+    reauth feature is intended for gcloud to use only.
+    If reauth is enabled, `pyu2f` dependency has to be installed in order to use security
+    key reauth feature. Dependency can be installed via `pip install pyu2f` or `pip install
+    google-auth[reauth]`.
+    """
+
+    def __init__(
+        self,
+        token,
+        refresh_token=None,
+        id_token=None,
+        token_uri=None,
+        client_id=None,
+        client_secret=None,
+        scopes=None,
+        default_scopes=None,
+        quota_project_id=None,
+        expiry=None,
+        rapt_token=None,
+        refresh_handler=None,
+        enable_reauth_refresh=False,
+    ):
+        """
+        Args:
+            token (Optional(str)): The OAuth 2.0 access token. Can be None
+                if refresh information is provided.
+            refresh_token (str): The OAuth 2.0 refresh token. If specified,
+                credentials can be refreshed.
+            id_token (str): The Open ID Connect ID Token.
+            token_uri (str): The OAuth 2.0 authorization server's token
+                endpoint URI. Must be specified for refresh, can be left as
+                None if the token can not be refreshed.
+            client_id (str): The OAuth 2.0 client ID. Must be specified for
+                refresh, can be left as None if the token can not be refreshed.
+            client_secret(str): The OAuth 2.0 client secret. Must be specified
+                for refresh, can be left as None if the token can not be
+                refreshed.
+            scopes (Sequence[str]): The scopes used to obtain authorization.
+                This parameter is used by :meth:`has_scopes`. OAuth 2.0
+                credentials can not request additional scopes after
+                authorization. The scopes must be derivable from the refresh
+                token if refresh information is provided (e.g. The refresh
+                token scopes are a superset of this or contain a wild card
+                scope like 'https://www.googleapis.com/auth/any-api').
+            default_scopes (Sequence[str]): Default scopes passed by a
+                Google client library. Use 'scopes' for user-defined scopes.
+            quota_project_id (Optional[str]): The project ID used for quota and billing.
+                This project may be different from the project used to
+                create the credentials.
+            rapt_token (Optional[str]): The reauth Proof Token.
+            refresh_handler (Optional[Callable[[google.auth.transport.Request, Sequence[str]], [str, datetime]]]):
+                A callable which takes in the HTTP request callable and the list of
+                OAuth scopes and when called returns an access token string for the
+                requested scopes and its expiry datetime. This is useful when no
+                refresh tokens are provided and tokens are obtained by calling
+                some external process on demand. It is particularly useful for
+                retrieving downscoped tokens from a token broker.
+            enable_reauth_refresh (Optional[bool]): Whether reauth refresh flow
+                should be used. This flag is for gcloud to use only.
+        """
+        super(Credentials, self).__init__()
+        self.token = token
+        self.expiry = expiry
+        self._refresh_token = refresh_token
+        self._id_token = id_token
+        self._scopes = scopes
+        self._default_scopes = default_scopes
+        self._token_uri = token_uri
+        self._client_id = client_id
+        self._client_secret = client_secret
+        self._quota_project_id = quota_project_id
+        self._rapt_token = rapt_token
+        self.refresh_handler = refresh_handler
+        self._enable_reauth_refresh = enable_reauth_refresh
+
+    def __getstate__(self):
+        """A __getstate__ method must exist for the __setstate__ to be called
+        This is identical to the default implementation.
+        See https://docs.python.org/3.7/library/pickle.html#object.__setstate__
+        """
+        state_dict = self.__dict__.copy()
+        # Remove _refresh_handler function as there are limitations pickling and
+        # unpickling certain callables (lambda, functools.partial instances)
+        # because they need to be importable.
+        # Instead, the refresh_handler setter should be used to repopulate this.
+        del state_dict["_refresh_handler"]
+        return state_dict
+
+    def __setstate__(self, d):
+        """Credentials pickled with older versions of the class do not have
+        all the attributes."""
+        self.token = d.get("token")
+        self.expiry = d.get("expiry")
+        self._refresh_token = d.get("_refresh_token")
+        self._id_token = d.get("_id_token")
+        self._scopes = d.get("_scopes")
+        self._default_scopes = d.get("_default_scopes")
+        self._token_uri = d.get("_token_uri")
+        self._client_id = d.get("_client_id")
+        self._client_secret = d.get("_client_secret")
+        self._quota_project_id = d.get("_quota_project_id")
+        self._rapt_token = d.get("_rapt_token")
+        self._enable_reauth_refresh = d.get("_enable_reauth_refresh")
+        # The refresh_handler setter should be used to repopulate this.
+        self._refresh_handler = None
+
+    @property
+    def refresh_token(self):
+        """Optional[str]: The OAuth 2.0 refresh token."""
+        return self._refresh_token
+
+    @property
+    def scopes(self):
+        """Optional[str]: The OAuth 2.0 permission scopes."""
+        return self._scopes
+
+    @property
+    def token_uri(self):
+        """Optional[str]: The OAuth 2.0 authorization server's token endpoint
+        URI."""
+        return self._token_uri
+
+    @property
+    def id_token(self):
+        """Optional[str]: The Open ID Connect ID Token.
+
+        Depending on the authorization server and the scopes requested, this
+        may be populated when credentials are obtained and updated when
+        :meth:`refresh` is called. This token is a JWT. It can be verified
+        and decoded using :func:`google.oauth2.id_token.verify_oauth2_token`.
+        """
+        return self._id_token
+
+    @property
+    def client_id(self):
+        """Optional[str]: The OAuth 2.0 client ID."""
+        return self._client_id
+
+    @property
+    def client_secret(self):
+        """Optional[str]: The OAuth 2.0 client secret."""
+        return self._client_secret
+
+    @property
+    def requires_scopes(self):
+        """False: OAuth 2.0 credentials have their scopes set when
+        the initial token is requested and can not be changed."""
+        return False
+
+    @property
+    def rapt_token(self):
+        """Optional[str]: The reauth Proof Token."""
+        return self._rapt_token
+
+    @property
+    def refresh_handler(self):
+        """Returns the refresh handler if available.
+
+        Returns:
+           Optional[Callable[[google.auth.transport.Request, Sequence[str]], [str, datetime]]]:
+               The current refresh handler.
+        """
+        return self._refresh_handler
+
+    @refresh_handler.setter
+    def refresh_handler(self, value):
+        """Updates the current refresh handler.
+
+        Args:
+            value (Optional[Callable[[google.auth.transport.Request, Sequence[str]], [str, datetime]]]):
+                The updated value of the refresh handler.
+
+        Raises:
+            TypeError: If the value is not a callable or None.
+        """
+        if not callable(value) and value is not None:
+            raise TypeError("The provided refresh_handler is not a callable or None.")
+        self._refresh_handler = value
+
+    @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
+    def with_quota_project(self, quota_project_id):
+
+        return self.__class__(
+            self.token,
+            refresh_token=self.refresh_token,
+            id_token=self.id_token,
+            token_uri=self.token_uri,
+            client_id=self.client_id,
+            client_secret=self.client_secret,
+            scopes=self.scopes,
+            default_scopes=self.default_scopes,
+            quota_project_id=quota_project_id,
+            rapt_token=self.rapt_token,
+            enable_reauth_refresh=self._enable_reauth_refresh,
+        )
+
+    @_helpers.copy_docstring(credentials.Credentials)
+    def refresh(self, request):
+        scopes = self._scopes if self._scopes is not None else self._default_scopes
+        # Use refresh handler if available and no refresh token is
+        # available. This is useful in general when tokens are obtained by calling
+        # some external process on demand. It is particularly useful for retrieving
+        # downscoped tokens from a token broker.
+        if self._refresh_token is None and self.refresh_handler:
+            token, expiry = self.refresh_handler(request, scopes=scopes)
+            # Validate returned data.
+            if not isinstance(token, six.string_types):
+                raise exceptions.RefreshError(
+                    "The refresh_handler returned token is not a string."
+                )
+            if not isinstance(expiry, datetime):
+                raise exceptions.RefreshError(
+                    "The refresh_handler returned expiry is not a datetime object."
+                )
+            if _helpers.utcnow() >= expiry - _helpers.REFRESH_THRESHOLD:
+                raise exceptions.RefreshError(
+                    "The credentials returned by the refresh_handler are "
+                    "already expired."
+                )
+            self.token = token
+            self.expiry = expiry
+            return
+
+        if (
+            self._refresh_token is None
+            or self._token_uri is None
+            or self._client_id is None
+            or self._client_secret is None
+        ):
+            raise exceptions.RefreshError(
+                "The credentials do not contain the necessary fields need to "
+                "refresh the access token. You must specify refresh_token, "
+                "token_uri, client_id, and client_secret."
+            )
+
+        (
+            access_token,
+            refresh_token,
+            expiry,
+            grant_response,
+            rapt_token,
+        ) = reauth.refresh_grant(
+            request,
+            self._token_uri,
+            self._refresh_token,
+            self._client_id,
+            self._client_secret,
+            scopes=scopes,
+            rapt_token=self._rapt_token,
+            enable_reauth_refresh=self._enable_reauth_refresh,
+        )
+
+        self.token = access_token
+        self.expiry = expiry
+        self._refresh_token = refresh_token
+        self._id_token = grant_response.get("id_token")
+        self._rapt_token = rapt_token
+
+        if scopes and "scope" in grant_response:
+            requested_scopes = frozenset(scopes)
+            granted_scopes = frozenset(grant_response["scope"].split())
+            scopes_requested_but_not_granted = requested_scopes - granted_scopes
+            if scopes_requested_but_not_granted:
+                raise exceptions.RefreshError(
+                    "Not all requested scopes were granted by the "
+                    "authorization server, missing scopes {}.".format(
+                        ", ".join(scopes_requested_but_not_granted)
+                    )
+                )
+
+    @classmethod
+    def from_authorized_user_info(cls, info, scopes=None):
+        """Creates a Credentials instance from parsed authorized user info.
+
+        Args:
+            info (Mapping[str, str]): The authorized user info in Google
+                format.
+            scopes (Sequence[str]): Optional list of scopes to include in the
+                credentials.
+
+        Returns:
+            google.oauth2.credentials.Credentials: The constructed
+                credentials.
+
+        Raises:
+            ValueError: If the info is not in the expected format.
+        """
+        keys_needed = set(("refresh_token", "client_id", "client_secret"))
+        missing = keys_needed.difference(six.iterkeys(info))
+
+        if missing:
+            raise ValueError(
+                "Authorized user info was not in the expected format, missing "
+                "fields {}.".format(", ".join(missing))
+            )
+
+        # access token expiry (datetime obj); auto-expire if not saved
+        expiry = info.get("expiry")
+        if expiry:
+            expiry = datetime.strptime(
+                expiry.rstrip("Z").split(".")[0], "%Y-%m-%dT%H:%M:%S"
+            )
+        else:
+            expiry = _helpers.utcnow() - _helpers.REFRESH_THRESHOLD
+
+        # process scopes, which needs to be a seq
+        if scopes is None and "scopes" in info:
+            scopes = info.get("scopes")
+            if isinstance(scopes, six.string_types):
+                scopes = scopes.split(" ")
+
+        return cls(
+            token=info.get("token"),
+            refresh_token=info.get("refresh_token"),
+            token_uri=_GOOGLE_OAUTH2_TOKEN_ENDPOINT,  # always overrides
+            scopes=scopes,
+            client_id=info.get("client_id"),
+            client_secret=info.get("client_secret"),
+            quota_project_id=info.get("quota_project_id"),  # may not exist
+            expiry=expiry,
+            rapt_token=info.get("rapt_token"),  # may not exist
+        )
+
+    @classmethod
+    def from_authorized_user_file(cls, filename, scopes=None):
+        """Creates a Credentials instance from an authorized user json file.
+
+        Args:
+            filename (str): The path to the authorized user json file.
+            scopes (Sequence[str]): Optional list of scopes to include in the
+                credentials.
+
+        Returns:
+            google.oauth2.credentials.Credentials: The constructed
+                credentials.
+
+        Raises:
+            ValueError: If the file is not in the expected format.
+        """
+        with io.open(filename, "r", encoding="utf-8") as json_file:
+            data = json.load(json_file)
+            return cls.from_authorized_user_info(data, scopes)
+
+    def to_json(self, strip=None):
+        """Utility function that creates a JSON representation of a Credentials
+        object.
+
+        Args:
+            strip (Sequence[str]): Optional list of members to exclude from the
+                                   generated JSON.
+
+        Returns:
+            str: A JSON representation of this instance. When converted into
+            a dictionary, it can be passed to from_authorized_user_info()
+            to create a new credential instance.
+        """
+        prep = {
+            "token": self.token,
+            "refresh_token": self.refresh_token,
+            "token_uri": self.token_uri,
+            "client_id": self.client_id,
+            "client_secret": self.client_secret,
+            "scopes": self.scopes,
+            "rapt_token": self.rapt_token,
+        }
+        if self.expiry:  # flatten expiry timestamp
+            prep["expiry"] = self.expiry.isoformat() + "Z"
+
+        # Remove empty entries (those which are None)
+        prep = {k: v for k, v in prep.items() if v is not None}
+
+        # Remove entries that explicitely need to be removed
+        if strip is not None:
+            prep = {k: v for k, v in prep.items() if k not in strip}
+
+        return json.dumps(prep)
+
+
+class UserAccessTokenCredentials(credentials.CredentialsWithQuotaProject):
+    """Access token credentials for user account.
+
+    Obtain the access token for a given user account or the current active
+    user account with the ``gcloud auth print-access-token`` command.
+
+    Args:
+        account (Optional[str]): Account to get the access token for. If not
+            specified, the current active account will be used.
+        quota_project_id (Optional[str]): The project ID used for quota
+            and billing.
+    """
+
+    def __init__(self, account=None, quota_project_id=None):
+        super(UserAccessTokenCredentials, self).__init__()
+        self._account = account
+        self._quota_project_id = quota_project_id
+
+    def with_account(self, account):
+        """Create a new instance with the given account.
+
+        Args:
+            account (str): Account to get the access token for.
+
+        Returns:
+            google.oauth2.credentials.UserAccessTokenCredentials: The created
+                credentials with the given account.
+        """
+        return self.__class__(account=account, quota_project_id=self._quota_project_id)
+
+    @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
+    def with_quota_project(self, quota_project_id):
+        return self.__class__(account=self._account, quota_project_id=quota_project_id)
+
+    def refresh(self, request):
+        """Refreshes the access token.
+
+        Args:
+            request (google.auth.transport.Request): This argument is required
+                by the base class interface but not used in this implementation,
+                so just set it to `None`.
+
+        Raises:
+            google.auth.exceptions.UserAccessTokenError: If the access token
+                refresh failed.
+        """
+        self.token = _cloud_sdk.get_auth_access_token(self._account)
+
+    @_helpers.copy_docstring(credentials.Credentials)
+    def before_request(self, request, method, url, headers):
+        self.refresh(request)
+        self.apply(headers)
diff --git a/google/oauth2/id_token.py b/google/oauth2/id_token.py
new file mode 100644
index 0000000..74899ae
--- /dev/null
+++ b/google/oauth2/id_token.py
@@ -0,0 +1,340 @@
+# Copyright 2016 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.
+
+"""Google ID Token helpers.
+
+Provides support for verifying `OpenID Connect ID Tokens`_, especially ones
+generated by Google infrastructure.
+
+To parse and verify an ID Token issued by Google's OAuth 2.0 authorization
+server use :func:`verify_oauth2_token`. To verify an ID Token issued by
+Firebase, use :func:`verify_firebase_token`.
+
+A general purpose ID Token verifier is available as :func:`verify_token`.
+
+Example::
+
+    from google.oauth2 import id_token
+    from google.auth.transport import requests
+
+    request = requests.Request()
+
+    id_info = id_token.verify_oauth2_token(
+        token, request, 'my-client-id.example.com')
+
+    userid = id_info['sub']
+
+By default, this will re-fetch certificates for each verification. Because
+Google's public keys are only changed infrequently (on the order of once per
+day), you may wish to take advantage of caching to reduce latency and the
+potential for network errors. This can be accomplished using an external
+library like `CacheControl`_ to create a cache-aware
+:class:`google.auth.transport.Request`::
+
+    import cachecontrol
+    import google.auth.transport.requests
+    import requests
+
+    session = requests.session()
+    cached_session = cachecontrol.CacheControl(session)
+    request = google.auth.transport.requests.Request(session=cached_session)
+
+.. _OpenID Connect ID Tokens:
+    http://openid.net/specs/openid-connect-core-1_0.html#IDToken
+.. _CacheControl: https://cachecontrol.readthedocs.io
+"""
+
+import json
+import os
+
+import six
+from six.moves import http_client
+
+from google.auth import environment_vars
+from google.auth import exceptions
+from google.auth import jwt
+import google.auth.transport.requests
+
+
+# The URL that provides public certificates for verifying ID tokens issued
+# by Google's OAuth 2.0 authorization server.
+_GOOGLE_OAUTH2_CERTS_URL = "https://www.googleapis.com/oauth2/v1/certs"
+
+# The URL that provides public certificates for verifying ID tokens issued
+# by Firebase and the Google APIs infrastructure
+_GOOGLE_APIS_CERTS_URL = (
+    "https://www.googleapis.com/robot/v1/metadata/x509"
+    "/[email protected]"
+)
+
+_GOOGLE_ISSUERS = ["accounts.google.com", "https://accounts.google.com"]
+
+
+def _fetch_certs(request, certs_url):
+    """Fetches certificates.
+
+    Google-style cerificate endpoints return JSON in the format of
+    ``{'key id': 'x509 certificate'}``.
+
+    Args:
+        request (google.auth.transport.Request): The object used to make
+            HTTP requests.
+        certs_url (str): The certificate endpoint URL.
+
+    Returns:
+        Mapping[str, str]: A mapping of public key ID to x.509 certificate
+            data.
+    """
+    response = request(certs_url, method="GET")
+
+    if response.status != http_client.OK:
+        raise exceptions.TransportError(
+            "Could not fetch certificates at {}".format(certs_url)
+        )
+
+    return json.loads(response.data.decode("utf-8"))
+
+
+def verify_token(
+    id_token,
+    request,
+    audience=None,
+    certs_url=_GOOGLE_OAUTH2_CERTS_URL,
+    clock_skew_in_seconds=0,
+):
+    """Verifies an ID token and returns the decoded token.
+
+    Args:
+        id_token (Union[str, bytes]): The encoded token.
+        request (google.auth.transport.Request): The object used to make
+            HTTP requests.
+        audience (str or list): The audience or audiences that this token is
+            intended for. If None then the audience is not verified.
+        certs_url (str): The URL that specifies the certificates to use to
+            verify the token. This URL should return JSON in the format of
+            ``{'key id': 'x509 certificate'}``.
+        clock_skew_in_seconds (int): The clock skew used for `iat` and `exp`
+            validation.
+
+    Returns:
+        Mapping[str, Any]: The decoded token.
+    """
+    certs = _fetch_certs(request, certs_url)
+
+    return jwt.decode(
+        id_token,
+        certs=certs,
+        audience=audience,
+        clock_skew_in_seconds=clock_skew_in_seconds,
+    )
+
+
+def verify_oauth2_token(id_token, request, audience=None, clock_skew_in_seconds=0):
+    """Verifies an ID Token issued by Google's OAuth 2.0 authorization server.
+
+    Args:
+        id_token (Union[str, bytes]): The encoded token.
+        request (google.auth.transport.Request): The object used to make
+            HTTP requests.
+        audience (str): The audience that this token is intended for. This is
+            typically your application's OAuth 2.0 client ID. If None then the
+            audience is not verified.
+        clock_skew_in_seconds (int): The clock skew used for `iat` and `exp`
+            validation.
+
+    Returns:
+        Mapping[str, Any]: The decoded token.
+
+    Raises:
+        exceptions.GoogleAuthError: If the issuer is invalid.
+    """
+    idinfo = verify_token(
+        id_token,
+        request,
+        audience=audience,
+        certs_url=_GOOGLE_OAUTH2_CERTS_URL,
+        clock_skew_in_seconds=clock_skew_in_seconds,
+    )
+
+    if idinfo["iss"] not in _GOOGLE_ISSUERS:
+        raise exceptions.GoogleAuthError(
+            "Wrong issuer. 'iss' should be one of the following: {}".format(
+                _GOOGLE_ISSUERS
+            )
+        )
+
+    return idinfo
+
+
+def verify_firebase_token(id_token, request, audience=None, clock_skew_in_seconds=0):
+    """Verifies an ID Token issued by Firebase Authentication.
+
+    Args:
+        id_token (Union[str, bytes]): The encoded token.
+        request (google.auth.transport.Request): The object used to make
+            HTTP requests.
+        audience (str): The audience that this token is intended for. This is
+            typically your Firebase application ID. If None then the audience
+            is not verified.
+        clock_skew_in_seconds (int): The clock skew used for `iat` and `exp`
+            validation.
+
+    Returns:
+        Mapping[str, Any]: The decoded token.
+    """
+    return verify_token(
+        id_token,
+        request,
+        audience=audience,
+        certs_url=_GOOGLE_APIS_CERTS_URL,
+        clock_skew_in_seconds=clock_skew_in_seconds,
+    )
+
+
+def fetch_id_token_credentials(audience, request=None):
+    """Create the ID Token credentials from the current environment.
+
+    This function acquires ID token from the environment in the following order.
+    See https://google.aip.dev/auth/4110.
+
+    1. If the environment variable ``GOOGLE_APPLICATION_CREDENTIALS`` is set
+       to the path of a valid service account JSON file, then ID token is
+       acquired using this service account credentials.
+    2. If the application is running in Compute Engine, App Engine or Cloud Run,
+       then the ID token are obtained from the metadata server.
+    3. If metadata server doesn't exist and no valid service account credentials
+       are found, :class:`~google.auth.exceptions.DefaultCredentialsError` will
+       be raised.
+
+    Example::
+
+        import google.oauth2.id_token
+        import google.auth.transport.requests
+
+        request = google.auth.transport.requests.Request()
+        target_audience = "https://pubsub.googleapis.com"
+
+        # Create ID token credentials.
+        credentials = google.oauth2.id_token.fetch_id_token_credentials(target_audience, request=request)
+
+        # Refresh the credential to obtain an ID token.
+        credentials.refresh(request)
+
+        id_token = credentials.token
+        id_token_expiry = credentials.expiry
+
+    Args:
+        audience (str): The audience that this ID token is intended for.
+        request (Optional[google.auth.transport.Request]): A callable used to make
+            HTTP requests. A request object will be created if not provided.
+
+    Returns:
+        google.auth.credentials.Credentials: The ID token credentials.
+
+    Raises:
+        ~google.auth.exceptions.DefaultCredentialsError:
+            If metadata server doesn't exist and no valid service account
+            credentials are found.
+    """
+    # 1. Try to get credentials from the GOOGLE_APPLICATION_CREDENTIALS environment
+    # variable.
+    credentials_filename = os.environ.get(environment_vars.CREDENTIALS)
+    if credentials_filename:
+        if not (
+            os.path.exists(credentials_filename)
+            and os.path.isfile(credentials_filename)
+        ):
+            raise exceptions.DefaultCredentialsError(
+                "GOOGLE_APPLICATION_CREDENTIALS path is either not found or invalid."
+            )
+
+        try:
+            with open(credentials_filename, "r") as f:
+                from google.oauth2 import service_account
+
+                info = json.load(f)
+                if info.get("type") == "service_account":
+                    return service_account.IDTokenCredentials.from_service_account_info(
+                        info, target_audience=audience
+                    )
+        except ValueError as caught_exc:
+            new_exc = exceptions.DefaultCredentialsError(
+                "GOOGLE_APPLICATION_CREDENTIALS is not valid service account credentials.",
+                caught_exc,
+            )
+            six.raise_from(new_exc, caught_exc)
+
+    # 2. Try to fetch ID token from metada server if it exists. The code
+    # works for GAE and Cloud Run metadata server as well.
+    try:
+        from google.auth import compute_engine
+        from google.auth.compute_engine import _metadata
+
+        # Create a request object if not provided.
+        if not request:
+            request = google.auth.transport.requests.Request()
+
+        if _metadata.ping(request):
+            return compute_engine.IDTokenCredentials(
+                request, audience, use_metadata_identity_endpoint=True
+            )
+    except (ImportError, exceptions.TransportError):
+        pass
+
+    raise exceptions.DefaultCredentialsError(
+        "Neither metadata server or valid service account credentials are found."
+    )
+
+
+def fetch_id_token(request, audience):
+    """Fetch the ID Token from the current environment.
+
+    This function acquires ID token from the environment in the following order.
+    See https://google.aip.dev/auth/4110.
+
+    1. If the environment variable ``GOOGLE_APPLICATION_CREDENTIALS`` is set
+       to the path of a valid service account JSON file, then ID token is
+       acquired using this service account credentials.
+    2. If the application is running in Compute Engine, App Engine or Cloud Run,
+       then the ID token are obtained from the metadata server.
+    3. If metadata server doesn't exist and no valid service account credentials
+       are found, :class:`~google.auth.exceptions.DefaultCredentialsError` will
+       be raised.
+
+    Example::
+
+        import google.oauth2.id_token
+        import google.auth.transport.requests
+
+        request = google.auth.transport.requests.Request()
+        target_audience = "https://pubsub.googleapis.com"
+
+        id_token = google.oauth2.id_token.fetch_id_token(request, target_audience)
+
+    Args:
+        request (google.auth.transport.Request): A callable used to make
+            HTTP requests.
+        audience (str): The audience that this ID token is intended for.
+
+    Returns:
+        str: The ID token.
+
+    Raises:
+        ~google.auth.exceptions.DefaultCredentialsError:
+            If metadata server doesn't exist and no valid service account
+            credentials are found.
+    """
+    id_token_credentials = fetch_id_token_credentials(audience, request=request)
+    id_token_credentials.refresh(request)
+    return id_token_credentials.token
diff --git a/google/oauth2/reauth.py b/google/oauth2/reauth.py
new file mode 100644
index 0000000..cbf1d7f
--- /dev/null
+++ b/google/oauth2/reauth.py
@@ -0,0 +1,350 @@
+# 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.
+
+"""A module that provides functions for handling rapt authentication.
+
+Reauth is a process of obtaining additional authentication (such as password,
+security token, etc.) while refreshing OAuth 2.0 credentials for a user.
+
+Credentials that use the Reauth flow must have the reauth scope,
+``https://www.googleapis.com/auth/accounts.reauth``.
+
+This module provides a high-level function for executing the Reauth process,
+:func:`refresh_grant`, and lower-level helpers for doing the individual
+steps of the reauth process.
+
+Those steps are:
+
+1. Obtaining a list of challenges from the reauth server.
+2. Running through each challenge and sending the result back to the reauth
+   server.
+3. Refreshing the access token using the returned rapt token.
+"""
+
+import sys
+
+from six.moves import range
+
+from google.auth import exceptions
+from google.oauth2 import _client
+from google.oauth2 import challenges
+
+
+_REAUTH_SCOPE = "https://www.googleapis.com/auth/accounts.reauth"
+_REAUTH_API = "https://reauth.googleapis.com/v2/sessions"
+
+_REAUTH_NEEDED_ERROR = "invalid_grant"
+_REAUTH_NEEDED_ERROR_INVALID_RAPT = "invalid_rapt"
+_REAUTH_NEEDED_ERROR_RAPT_REQUIRED = "rapt_required"
+
+_AUTHENTICATED = "AUTHENTICATED"
+_CHALLENGE_REQUIRED = "CHALLENGE_REQUIRED"
+_CHALLENGE_PENDING = "CHALLENGE_PENDING"
+
+
+# Override this global variable to set custom max number of rounds of reauth
+# challenges should be run.
+RUN_CHALLENGE_RETRY_LIMIT = 5
+
+
+def is_interactive():
+    """Check if we are in an interractive environment.
+
+    Override this function with a different logic if you are using this library
+    outside a CLI.
+
+    If the rapt token needs refreshing, the user needs to answer the challenges.
+    If the user is not in an interractive environment, the challenges can not
+    be answered and we just wait for timeout for no reason.
+
+    Returns:
+        bool: True if is interactive environment, False otherwise.
+    """
+
+    return sys.stdin.isatty()
+
+
+def _get_challenges(
+    request, supported_challenge_types, access_token, requested_scopes=None
+):
+    """Does initial request to reauth API to get the challenges.
+
+    Args:
+        request (google.auth.transport.Request): A callable used to make
+            HTTP requests.
+        supported_challenge_types (Sequence[str]): list of challenge names
+            supported by the manager.
+        access_token (str): Access token with reauth scopes.
+        requested_scopes (Optional(Sequence[str])): Authorized scopes for the credentials.
+
+    Returns:
+        dict: The response from the reauth API.
+    """
+    body = {"supportedChallengeTypes": supported_challenge_types}
+    if requested_scopes:
+        body["oauthScopesForDomainPolicyLookup"] = requested_scopes
+
+    return _client._token_endpoint_request(
+        request, _REAUTH_API + ":start", body, access_token=access_token, use_json=True
+    )
+
+
+def _send_challenge_result(
+    request, session_id, challenge_id, client_input, access_token
+):
+    """Attempt to refresh access token by sending next challenge result.
+
+    Args:
+        request (google.auth.transport.Request): A callable used to make
+            HTTP requests.
+        session_id (str): session id returned by the initial reauth call.
+        challenge_id (str): challenge id returned by the initial reauth call.
+        client_input: dict with a challenge-specific client input. For example:
+            ``{'credential': password}`` for password challenge.
+        access_token (str): Access token with reauth scopes.
+
+    Returns:
+        dict: The response from the reauth API.
+    """
+    body = {
+        "sessionId": session_id,
+        "challengeId": challenge_id,
+        "action": "RESPOND",
+        "proposalResponse": client_input,
+    }
+
+    return _client._token_endpoint_request(
+        request,
+        _REAUTH_API + "/{}:continue".format(session_id),
+        body,
+        access_token=access_token,
+        use_json=True,
+    )
+
+
+def _run_next_challenge(msg, request, access_token):
+    """Get the next challenge from msg and run it.
+
+    Args:
+        msg (dict): Reauth API response body (either from the initial request to
+            https://reauth.googleapis.com/v2/sessions:start or from sending the
+            previous challenge response to
+            https://reauth.googleapis.com/v2/sessions/id:continue)
+        request (google.auth.transport.Request): A callable used to make
+            HTTP requests.
+        access_token (str): reauth access token
+
+    Returns:
+        dict: The response from the reauth API.
+
+    Raises:
+        google.auth.exceptions.ReauthError: if reauth failed.
+    """
+    for challenge in msg["challenges"]:
+        if challenge["status"] != "READY":
+            # Skip non-activated challenges.
+            continue
+        c = challenges.AVAILABLE_CHALLENGES.get(challenge["challengeType"], None)
+        if not c:
+            raise exceptions.ReauthFailError(
+                "Unsupported challenge type {0}. Supported types: {1}".format(
+                    challenge["challengeType"],
+                    ",".join(list(challenges.AVAILABLE_CHALLENGES.keys())),
+                )
+            )
+        if not c.is_locally_eligible:
+            raise exceptions.ReauthFailError(
+                "Challenge {0} is not locally eligible".format(
+                    challenge["challengeType"]
+                )
+            )
+        client_input = c.obtain_challenge_input(challenge)
+        if not client_input:
+            return None
+        return _send_challenge_result(
+            request,
+            msg["sessionId"],
+            challenge["challengeId"],
+            client_input,
+            access_token,
+        )
+    return None
+
+
+def _obtain_rapt(request, access_token, requested_scopes):
+    """Given an http request method and reauth access token, get rapt token.
+
+    Args:
+        request (google.auth.transport.Request): A callable used to make
+            HTTP requests.
+        access_token (str): reauth access token
+        requested_scopes (Sequence[str]): scopes required by the client application
+
+    Returns:
+        str: The rapt token.
+
+    Raises:
+        google.auth.exceptions.ReauthError: if reauth failed
+    """
+    msg = _get_challenges(
+        request,
+        list(challenges.AVAILABLE_CHALLENGES.keys()),
+        access_token,
+        requested_scopes,
+    )
+
+    if msg["status"] == _AUTHENTICATED:
+        return msg["encodedProofOfReauthToken"]
+
+    for _ in range(0, RUN_CHALLENGE_RETRY_LIMIT):
+        if not (
+            msg["status"] == _CHALLENGE_REQUIRED or msg["status"] == _CHALLENGE_PENDING
+        ):
+            raise exceptions.ReauthFailError(
+                "Reauthentication challenge failed due to API error: {}".format(
+                    msg["status"]
+                )
+            )
+
+        if not is_interactive():
+            raise exceptions.ReauthFailError(
+                "Reauthentication challenge could not be answered because you are not"
+                " in an interactive session."
+            )
+
+        msg = _run_next_challenge(msg, request, access_token)
+
+        if msg["status"] == _AUTHENTICATED:
+            return msg["encodedProofOfReauthToken"]
+
+    # If we got here it means we didn't get authenticated.
+    raise exceptions.ReauthFailError("Failed to obtain rapt token.")
+
+
+def get_rapt_token(
+    request, client_id, client_secret, refresh_token, token_uri, scopes=None
+):
+    """Given an http request method and refresh_token, get rapt token.
+
+    Args:
+        request (google.auth.transport.Request): A callable used to make
+            HTTP requests.
+        client_id (str): client id to get access token for reauth scope.
+        client_secret (str): client secret for the client_id
+        refresh_token (str): refresh token to refresh access token
+        token_uri (str): uri to refresh access token
+        scopes (Optional(Sequence[str])): scopes required by the client application
+
+    Returns:
+        str: The rapt token.
+    Raises:
+        google.auth.exceptions.RefreshError: If reauth failed.
+    """
+    sys.stderr.write("Reauthentication required.\n")
+
+    # Get access token for reauth.
+    access_token, _, _, _ = _client.refresh_grant(
+        request=request,
+        client_id=client_id,
+        client_secret=client_secret,
+        refresh_token=refresh_token,
+        token_uri=token_uri,
+        scopes=[_REAUTH_SCOPE],
+    )
+
+    # Get rapt token from reauth API.
+    rapt_token = _obtain_rapt(request, access_token, requested_scopes=scopes)
+
+    return rapt_token
+
+
+def refresh_grant(
+    request,
+    token_uri,
+    refresh_token,
+    client_id,
+    client_secret,
+    scopes=None,
+    rapt_token=None,
+    enable_reauth_refresh=False,
+):
+    """Implements the reauthentication flow.
+
+    Args:
+        request (google.auth.transport.Request): A callable used to make
+            HTTP requests.
+        token_uri (str): The OAuth 2.0 authorizations server's token endpoint
+            URI.
+        refresh_token (str): The refresh token to use to get a new access
+            token.
+        client_id (str): The OAuth 2.0 application's client ID.
+        client_secret (str): The Oauth 2.0 appliaction's client secret.
+        scopes (Optional(Sequence[str])): Scopes to request. If present, all
+            scopes must be authorized for the refresh token. Useful if refresh
+            token has a wild card scope (e.g.
+            'https://www.googleapis.com/auth/any-api').
+        rapt_token (Optional(str)): The rapt token for reauth.
+        enable_reauth_refresh (Optional[bool]): Whether reauth refresh flow
+            should be used. The default value is False. This option is for
+            gcloud only, other users should use the default value.
+
+    Returns:
+        Tuple[str, Optional[str], Optional[datetime], Mapping[str, str], str]: The
+            access token, new refresh token, expiration, the additional data
+            returned by the token endpoint, and the rapt token.
+
+    Raises:
+        google.auth.exceptions.RefreshError: If the token endpoint returned
+            an error.
+    """
+    body = {
+        "grant_type": _client._REFRESH_GRANT_TYPE,
+        "client_id": client_id,
+        "client_secret": client_secret,
+        "refresh_token": refresh_token,
+    }
+    if scopes:
+        body["scope"] = " ".join(scopes)
+    if rapt_token:
+        body["rapt"] = rapt_token
+
+    response_status_ok, response_data = _client._token_endpoint_request_no_throw(
+        request, token_uri, body
+    )
+    if (
+        not response_status_ok
+        and response_data.get("error") == _REAUTH_NEEDED_ERROR
+        and (
+            response_data.get("error_subtype") == _REAUTH_NEEDED_ERROR_INVALID_RAPT
+            or response_data.get("error_subtype") == _REAUTH_NEEDED_ERROR_RAPT_REQUIRED
+        )
+    ):
+        if not enable_reauth_refresh:
+            raise exceptions.RefreshError(
+                "Reauthentication is needed. Please run `gcloud auth login --update-adc` to reauthenticate."
+            )
+
+        rapt_token = get_rapt_token(
+            request, client_id, client_secret, refresh_token, token_uri, scopes=scopes
+        )
+        body["rapt"] = rapt_token
+        (response_status_ok, response_data) = _client._token_endpoint_request_no_throw(
+            request, token_uri, body
+        )
+
+    if not response_status_ok:
+        _client._handle_error_response(response_data)
+    return _client._handle_refresh_grant_response(response_data, refresh_token) + (
+        rapt_token,
+    )
diff --git a/google/oauth2/service_account.py b/google/oauth2/service_account.py
new file mode 100644
index 0000000..ecaac03
--- /dev/null
+++ b/google/oauth2/service_account.py
@@ -0,0 +1,687 @@
+# Copyright 2016 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.
+
+"""Service Accounts: JSON Web Token (JWT) Profile for OAuth 2.0
+
+This module implements the JWT Profile for OAuth 2.0 Authorization Grants
+as defined by `RFC 7523`_ with particular support for how this RFC is
+implemented in Google's infrastructure. Google refers to these credentials
+as *Service Accounts*.
+
+Service accounts are used for server-to-server communication, such as
+interactions between a web application server and a Google service. The
+service account belongs to your application instead of to an individual end
+user. In contrast to other OAuth 2.0 profiles, no users are involved and your
+application "acts" as the service account.
+
+Typically an application uses a service account when the application uses
+Google APIs to work with its own data rather than a user's data. For example,
+an application that uses Google Cloud Datastore for data persistence would use
+a service account to authenticate its calls to the Google Cloud Datastore API.
+However, an application that needs to access a user's Drive documents would
+use the normal OAuth 2.0 profile.
+
+Additionally, Google Apps domain administrators can grant service accounts
+`domain-wide delegation`_ authority to access user data on behalf of users in
+the domain.
+
+This profile uses a JWT to acquire an OAuth 2.0 access token. The JWT is used
+in place of the usual authorization token returned during the standard
+OAuth 2.0 Authorization Code grant. The JWT is only used for this purpose, as
+the acquired access token is used as the bearer token when making requests
+using these credentials.
+
+This profile differs from normal OAuth 2.0 profile because no user consent
+step is required. The use of the private key allows this profile to assert
+identity directly.
+
+This profile also differs from the :mod:`google.auth.jwt` authentication
+because the JWT credentials use the JWT directly as the bearer token. This
+profile instead only uses the JWT to obtain an OAuth 2.0 access token. The
+obtained OAuth 2.0 access token is used as the bearer token.
+
+Domain-wide delegation
+----------------------
+
+Domain-wide delegation allows a service account to access user data on
+behalf of any user in a Google Apps domain without consent from the user.
+For example, an application that uses the Google Calendar API to add events to
+the calendars of all users in a Google Apps domain would use a service account
+to access the Google Calendar API on behalf of users.
+
+The Google Apps administrator must explicitly authorize the service account to
+do this. This authorization step is referred to as "delegating domain-wide
+authority" to a service account.
+
+You can use domain-wise delegation by creating a set of credentials with a
+specific subject using :meth:`~Credentials.with_subject`.
+
+.. _RFC 7523: https://tools.ietf.org/html/rfc7523
+"""
+
+import copy
+import datetime
+
+from google.auth import _helpers
+from google.auth import _service_account_info
+from google.auth import credentials
+from google.auth import jwt
+from google.oauth2 import _client
+
+_DEFAULT_TOKEN_LIFETIME_SECS = 3600  # 1 hour in seconds
+_GOOGLE_OAUTH2_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token"
+
+
+class Credentials(
+    credentials.Signing, credentials.Scoped, credentials.CredentialsWithQuotaProject
+):
+    """Service account credentials
+
+    Usually, you'll create these credentials with one of the helper
+    constructors. To create credentials using a Google service account
+    private key JSON file::
+
+        credentials = service_account.Credentials.from_service_account_file(
+            'service-account.json')
+
+    Or if you already have the service account file loaded::
+
+        service_account_info = json.load(open('service_account.json'))
+        credentials = service_account.Credentials.from_service_account_info(
+            service_account_info)
+
+    Both helper methods pass on arguments to the constructor, so you can
+    specify additional scopes and a subject if necessary::
+
+        credentials = service_account.Credentials.from_service_account_file(
+            'service-account.json',
+            scopes=['email'],
+            subject='[email protected]')
+
+    The credentials are considered immutable. If you want to modify the scopes
+    or the subject used for delegation, use :meth:`with_scopes` or
+    :meth:`with_subject`::
+
+        scoped_credentials = credentials.with_scopes(['email'])
+        delegated_credentials = credentials.with_subject(subject)
+
+    To add a quota project, use :meth:`with_quota_project`::
+
+        credentials = credentials.with_quota_project('myproject-123')
+    """
+
+    def __init__(
+        self,
+        signer,
+        service_account_email,
+        token_uri,
+        scopes=None,
+        default_scopes=None,
+        subject=None,
+        project_id=None,
+        quota_project_id=None,
+        additional_claims=None,
+        always_use_jwt_access=False,
+    ):
+        """
+        Args:
+            signer (google.auth.crypt.Signer): The signer used to sign JWTs.
+            service_account_email (str): The service account's email.
+            scopes (Sequence[str]): User-defined scopes to request during the
+                authorization grant.
+            default_scopes (Sequence[str]): Default scopes passed by a
+                Google client library. Use 'scopes' for user-defined scopes.
+            token_uri (str): The OAuth 2.0 Token URI.
+            subject (str): For domain-wide delegation, the email address of the
+                user to for which to request delegated access.
+            project_id  (str): Project ID associated with the service account
+                credential.
+            quota_project_id (Optional[str]): The project ID used for quota and
+                billing.
+            additional_claims (Mapping[str, str]): Any additional claims for
+                the JWT assertion used in the authorization grant.
+            always_use_jwt_access (Optional[bool]): Whether self signed JWT should
+                be always used.
+
+        .. note:: Typically one of the helper constructors
+            :meth:`from_service_account_file` or
+            :meth:`from_service_account_info` are used instead of calling the
+            constructor directly.
+        """
+        super(Credentials, self).__init__()
+
+        self._scopes = scopes
+        self._default_scopes = default_scopes
+        self._signer = signer
+        self._service_account_email = service_account_email
+        self._subject = subject
+        self._project_id = project_id
+        self._quota_project_id = quota_project_id
+        self._token_uri = token_uri
+        self._always_use_jwt_access = always_use_jwt_access
+
+        self._jwt_credentials = None
+
+        if additional_claims is not None:
+            self._additional_claims = additional_claims
+        else:
+            self._additional_claims = {}
+
+    @classmethod
+    def _from_signer_and_info(cls, signer, info, **kwargs):
+        """Creates a Credentials instance from a signer and service account
+        info.
+
+        Args:
+            signer (google.auth.crypt.Signer): The signer used to sign JWTs.
+            info (Mapping[str, str]): The service account info.
+            kwargs: Additional arguments to pass to the constructor.
+
+        Returns:
+            google.auth.jwt.Credentials: The constructed credentials.
+
+        Raises:
+            ValueError: If the info is not in the expected format.
+        """
+        return cls(
+            signer,
+            service_account_email=info["client_email"],
+            token_uri=info["token_uri"],
+            project_id=info.get("project_id"),
+            **kwargs
+        )
+
+    @classmethod
+    def from_service_account_info(cls, info, **kwargs):
+        """Creates a Credentials instance from parsed service account info.
+
+        Args:
+            info (Mapping[str, str]): The service account info in Google
+                format.
+            kwargs: Additional arguments to pass to the constructor.
+
+        Returns:
+            google.auth.service_account.Credentials: The constructed
+                credentials.
+
+        Raises:
+            ValueError: If the info is not in the expected format.
+        """
+        signer = _service_account_info.from_dict(
+            info, require=["client_email", "token_uri"]
+        )
+        return cls._from_signer_and_info(signer, info, **kwargs)
+
+    @classmethod
+    def from_service_account_file(cls, filename, **kwargs):
+        """Creates a Credentials instance from a service account json file.
+
+        Args:
+            filename (str): The path to the service account json file.
+            kwargs: Additional arguments to pass to the constructor.
+
+        Returns:
+            google.auth.service_account.Credentials: The constructed
+                credentials.
+        """
+        info, signer = _service_account_info.from_filename(
+            filename, require=["client_email", "token_uri"]
+        )
+        return cls._from_signer_and_info(signer, info, **kwargs)
+
+    @property
+    def service_account_email(self):
+        """The service account email."""
+        return self._service_account_email
+
+    @property
+    def project_id(self):
+        """Project ID associated with this credential."""
+        return self._project_id
+
+    @property
+    def requires_scopes(self):
+        """Checks if the credentials requires scopes.
+
+        Returns:
+            bool: True if there are no scopes set otherwise False.
+        """
+        return True if not self._scopes else False
+
+    @_helpers.copy_docstring(credentials.Scoped)
+    def with_scopes(self, scopes, default_scopes=None):
+        return self.__class__(
+            self._signer,
+            service_account_email=self._service_account_email,
+            scopes=scopes,
+            default_scopes=default_scopes,
+            token_uri=self._token_uri,
+            subject=self._subject,
+            project_id=self._project_id,
+            quota_project_id=self._quota_project_id,
+            additional_claims=self._additional_claims.copy(),
+            always_use_jwt_access=self._always_use_jwt_access,
+        )
+
+    def with_always_use_jwt_access(self, always_use_jwt_access):
+        """Create a copy of these credentials with the specified always_use_jwt_access value.
+
+        Args:
+            always_use_jwt_access (bool): Whether always use self signed JWT or not.
+
+        Returns:
+            google.auth.service_account.Credentials: A new credentials
+                instance.
+        """
+        return self.__class__(
+            self._signer,
+            service_account_email=self._service_account_email,
+            scopes=self._scopes,
+            default_scopes=self._default_scopes,
+            token_uri=self._token_uri,
+            subject=self._subject,
+            project_id=self._project_id,
+            quota_project_id=self._quota_project_id,
+            additional_claims=self._additional_claims.copy(),
+            always_use_jwt_access=always_use_jwt_access,
+        )
+
+    def with_subject(self, subject):
+        """Create a copy of these credentials with the specified subject.
+
+        Args:
+            subject (str): The subject claim.
+
+        Returns:
+            google.auth.service_account.Credentials: A new credentials
+                instance.
+        """
+        return self.__class__(
+            self._signer,
+            service_account_email=self._service_account_email,
+            scopes=self._scopes,
+            default_scopes=self._default_scopes,
+            token_uri=self._token_uri,
+            subject=subject,
+            project_id=self._project_id,
+            quota_project_id=self._quota_project_id,
+            additional_claims=self._additional_claims.copy(),
+            always_use_jwt_access=self._always_use_jwt_access,
+        )
+
+    def with_claims(self, additional_claims):
+        """Returns a copy of these credentials with modified claims.
+
+        Args:
+            additional_claims (Mapping[str, str]): Any additional claims for
+                the JWT payload. This will be merged with the current
+                additional claims.
+
+        Returns:
+            google.auth.service_account.Credentials: A new credentials
+                instance.
+        """
+        new_additional_claims = copy.deepcopy(self._additional_claims)
+        new_additional_claims.update(additional_claims or {})
+
+        return self.__class__(
+            self._signer,
+            service_account_email=self._service_account_email,
+            scopes=self._scopes,
+            default_scopes=self._default_scopes,
+            token_uri=self._token_uri,
+            subject=self._subject,
+            project_id=self._project_id,
+            quota_project_id=self._quota_project_id,
+            additional_claims=new_additional_claims,
+            always_use_jwt_access=self._always_use_jwt_access,
+        )
+
+    @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
+    def with_quota_project(self, quota_project_id):
+
+        return self.__class__(
+            self._signer,
+            service_account_email=self._service_account_email,
+            default_scopes=self._default_scopes,
+            scopes=self._scopes,
+            token_uri=self._token_uri,
+            subject=self._subject,
+            project_id=self._project_id,
+            quota_project_id=quota_project_id,
+            additional_claims=self._additional_claims.copy(),
+            always_use_jwt_access=self._always_use_jwt_access,
+        )
+
+    def _make_authorization_grant_assertion(self):
+        """Create the OAuth 2.0 assertion.
+
+        This assertion is used during the OAuth 2.0 grant to acquire an
+        access token.
+
+        Returns:
+            bytes: The authorization grant assertion.
+        """
+        now = _helpers.utcnow()
+        lifetime = datetime.timedelta(seconds=_DEFAULT_TOKEN_LIFETIME_SECS)
+        expiry = now + lifetime
+
+        payload = {
+            "iat": _helpers.datetime_to_secs(now),
+            "exp": _helpers.datetime_to_secs(expiry),
+            # The issuer must be the service account email.
+            "iss": self._service_account_email,
+            # The audience must be the auth token endpoint's URI
+            "aud": _GOOGLE_OAUTH2_TOKEN_ENDPOINT,
+            "scope": _helpers.scopes_to_string(self._scopes or ()),
+        }
+
+        payload.update(self._additional_claims)
+
+        # The subject can be a user email for domain-wide delegation.
+        if self._subject:
+            payload.setdefault("sub", self._subject)
+
+        token = jwt.encode(self._signer, payload)
+
+        return token
+
+    @_helpers.copy_docstring(credentials.Credentials)
+    def refresh(self, request):
+        # Since domain wide delegation doesn't work with self signed JWT. If
+        # subject exists, then we should not use self signed JWT.
+        if self._subject is None and self._jwt_credentials is not None:
+            self._jwt_credentials.refresh(request)
+            self.token = self._jwt_credentials.token
+            self.expiry = self._jwt_credentials.expiry
+        else:
+            assertion = self._make_authorization_grant_assertion()
+            access_token, expiry, _ = _client.jwt_grant(
+                request, self._token_uri, assertion
+            )
+            self.token = access_token
+            self.expiry = expiry
+
+    def _create_self_signed_jwt(self, audience):
+        """Create a self-signed JWT from the credentials if requirements are met.
+
+        Args:
+            audience (str): The service URL. ``https://[API_ENDPOINT]/``
+        """
+        # https://google.aip.dev/auth/4111
+        if self._always_use_jwt_access:
+            if self._scopes:
+                self._jwt_credentials = jwt.Credentials.from_signing_credentials(
+                    self, None, additional_claims={"scope": " ".join(self._scopes)}
+                )
+            elif audience:
+                self._jwt_credentials = jwt.Credentials.from_signing_credentials(
+                    self, audience
+                )
+            elif self._default_scopes:
+                self._jwt_credentials = jwt.Credentials.from_signing_credentials(
+                    self,
+                    None,
+                    additional_claims={"scope": " ".join(self._default_scopes)},
+                )
+        elif not self._scopes and audience:
+            self._jwt_credentials = jwt.Credentials.from_signing_credentials(
+                self, audience
+            )
+
+    @_helpers.copy_docstring(credentials.Signing)
+    def sign_bytes(self, message):
+        return self._signer.sign(message)
+
+    @property
+    @_helpers.copy_docstring(credentials.Signing)
+    def signer(self):
+        return self._signer
+
+    @property
+    @_helpers.copy_docstring(credentials.Signing)
+    def signer_email(self):
+        return self._service_account_email
+
+
+class IDTokenCredentials(credentials.Signing, credentials.CredentialsWithQuotaProject):
+    """Open ID Connect ID Token-based service account credentials.
+
+    These credentials are largely similar to :class:`.Credentials`, but instead
+    of using an OAuth 2.0 Access Token as the bearer token, they use an Open
+    ID Connect ID Token as the bearer token. These credentials are useful when
+    communicating to services that require ID Tokens and can not accept access
+    tokens.
+
+    Usually, you'll create these credentials with one of the helper
+    constructors. To create credentials using a Google service account
+    private key JSON file::
+
+        credentials = (
+            service_account.IDTokenCredentials.from_service_account_file(
+                'service-account.json'))
+
+
+    Or if you already have the service account file loaded::
+
+        service_account_info = json.load(open('service_account.json'))
+        credentials = (
+            service_account.IDTokenCredentials.from_service_account_info(
+                service_account_info))
+
+
+    Both helper methods pass on arguments to the constructor, so you can
+    specify additional scopes and a subject if necessary::
+
+        credentials = (
+            service_account.IDTokenCredentials.from_service_account_file(
+                'service-account.json',
+                scopes=['email'],
+                subject='[email protected]'))
+
+
+    The credentials are considered immutable. If you want to modify the scopes
+    or the subject used for delegation, use :meth:`with_scopes` or
+    :meth:`with_subject`::
+
+        scoped_credentials = credentials.with_scopes(['email'])
+        delegated_credentials = credentials.with_subject(subject)
+
+    """
+
+    def __init__(
+        self,
+        signer,
+        service_account_email,
+        token_uri,
+        target_audience,
+        additional_claims=None,
+        quota_project_id=None,
+    ):
+        """
+        Args:
+            signer (google.auth.crypt.Signer): The signer used to sign JWTs.
+            service_account_email (str): The service account's email.
+            token_uri (str): The OAuth 2.0 Token URI.
+            target_audience (str): The intended audience for these credentials,
+                used when requesting the ID Token. The ID Token's ``aud`` claim
+                will be set to this string.
+            additional_claims (Mapping[str, str]): Any additional claims for
+                the JWT assertion used in the authorization grant.
+            quota_project_id (Optional[str]): The project ID used for quota and billing.
+        .. note:: Typically one of the helper constructors
+            :meth:`from_service_account_file` or
+            :meth:`from_service_account_info` are used instead of calling the
+            constructor directly.
+        """
+        super(IDTokenCredentials, self).__init__()
+        self._signer = signer
+        self._service_account_email = service_account_email
+        self._token_uri = token_uri
+        self._target_audience = target_audience
+        self._quota_project_id = quota_project_id
+
+        if additional_claims is not None:
+            self._additional_claims = additional_claims
+        else:
+            self._additional_claims = {}
+
+    @classmethod
+    def _from_signer_and_info(cls, signer, info, **kwargs):
+        """Creates a credentials instance from a signer and service account
+        info.
+
+        Args:
+            signer (google.auth.crypt.Signer): The signer used to sign JWTs.
+            info (Mapping[str, str]): The service account info.
+            kwargs: Additional arguments to pass to the constructor.
+
+        Returns:
+            google.auth.jwt.IDTokenCredentials: The constructed credentials.
+
+        Raises:
+            ValueError: If the info is not in the expected format.
+        """
+        kwargs.setdefault("service_account_email", info["client_email"])
+        kwargs.setdefault("token_uri", info["token_uri"])
+        return cls(signer, **kwargs)
+
+    @classmethod
+    def from_service_account_info(cls, info, **kwargs):
+        """Creates a credentials instance from parsed service account info.
+
+        Args:
+            info (Mapping[str, str]): The service account info in Google
+                format.
+            kwargs: Additional arguments to pass to the constructor.
+
+        Returns:
+            google.auth.service_account.IDTokenCredentials: The constructed
+                credentials.
+
+        Raises:
+            ValueError: If the info is not in the expected format.
+        """
+        signer = _service_account_info.from_dict(
+            info, require=["client_email", "token_uri"]
+        )
+        return cls._from_signer_and_info(signer, info, **kwargs)
+
+    @classmethod
+    def from_service_account_file(cls, filename, **kwargs):
+        """Creates a credentials instance from a service account json file.
+
+        Args:
+            filename (str): The path to the service account json file.
+            kwargs: Additional arguments to pass to the constructor.
+
+        Returns:
+            google.auth.service_account.IDTokenCredentials: The constructed
+                credentials.
+        """
+        info, signer = _service_account_info.from_filename(
+            filename, require=["client_email", "token_uri"]
+        )
+        return cls._from_signer_and_info(signer, info, **kwargs)
+
+    def with_target_audience(self, target_audience):
+        """Create a copy of these credentials with the specified target
+        audience.
+
+        Args:
+            target_audience (str): The intended audience for these credentials,
+            used when requesting the ID Token.
+
+        Returns:
+            google.auth.service_account.IDTokenCredentials: A new credentials
+                instance.
+        """
+        return self.__class__(
+            self._signer,
+            service_account_email=self._service_account_email,
+            token_uri=self._token_uri,
+            target_audience=target_audience,
+            additional_claims=self._additional_claims.copy(),
+            quota_project_id=self.quota_project_id,
+        )
+
+    @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
+    def with_quota_project(self, quota_project_id):
+        return self.__class__(
+            self._signer,
+            service_account_email=self._service_account_email,
+            token_uri=self._token_uri,
+            target_audience=self._target_audience,
+            additional_claims=self._additional_claims.copy(),
+            quota_project_id=quota_project_id,
+        )
+
+    def _make_authorization_grant_assertion(self):
+        """Create the OAuth 2.0 assertion.
+
+        This assertion is used during the OAuth 2.0 grant to acquire an
+        ID token.
+
+        Returns:
+            bytes: The authorization grant assertion.
+        """
+        now = _helpers.utcnow()
+        lifetime = datetime.timedelta(seconds=_DEFAULT_TOKEN_LIFETIME_SECS)
+        expiry = now + lifetime
+
+        payload = {
+            "iat": _helpers.datetime_to_secs(now),
+            "exp": _helpers.datetime_to_secs(expiry),
+            # The issuer must be the service account email.
+            "iss": self.service_account_email,
+            # The audience must be the auth token endpoint's URI
+            "aud": _GOOGLE_OAUTH2_TOKEN_ENDPOINT,
+            # The target audience specifies which service the ID token is
+            # intended for.
+            "target_audience": self._target_audience,
+        }
+
+        payload.update(self._additional_claims)
+
+        token = jwt.encode(self._signer, payload)
+
+        return token
+
+    @_helpers.copy_docstring(credentials.Credentials)
+    def refresh(self, request):
+        assertion = self._make_authorization_grant_assertion()
+        access_token, expiry, _ = _client.id_token_jwt_grant(
+            request, self._token_uri, assertion
+        )
+        self.token = access_token
+        self.expiry = expiry
+
+    @property
+    def service_account_email(self):
+        """The service account email."""
+        return self._service_account_email
+
+    @_helpers.copy_docstring(credentials.Signing)
+    def sign_bytes(self, message):
+        return self._signer.sign(message)
+
+    @property
+    @_helpers.copy_docstring(credentials.Signing)
+    def signer(self):
+        return self._signer
+
+    @property
+    @_helpers.copy_docstring(credentials.Signing)
+    def signer_email(self):
+        return self._service_account_email
diff --git a/google/oauth2/sts.py b/google/oauth2/sts.py
new file mode 100644
index 0000000..ae3c014
--- /dev/null
+++ b/google/oauth2/sts.py
@@ -0,0 +1,155 @@
+# 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.
+
+"""OAuth 2.0 Token Exchange Spec.
+
+This module defines a token exchange utility based on the `OAuth 2.0 Token
+Exchange`_ spec. This will be mainly used to exchange external credentials
+for GCP access tokens in workload identity pools to access Google APIs.
+
+The implementation will support various types of client authentication as
+allowed in the spec.
+
+A deviation on the spec will be for additional Google specific options that
+cannot be easily mapped to parameters defined in the RFC.
+
+The returned dictionary response will be based on the `rfc8693 section 2.2.1`_
+spec JSON response.
+
+.. _OAuth 2.0 Token Exchange: https://tools.ietf.org/html/rfc8693
+.. _rfc8693 section 2.2.1: https://tools.ietf.org/html/rfc8693#section-2.2.1
+"""
+
+import json
+
+from six.moves import http_client
+from six.moves import urllib
+
+from google.oauth2 import utils
+
+
+_URLENCODED_HEADERS = {"Content-Type": "application/x-www-form-urlencoded"}
+
+
+class Client(utils.OAuthClientAuthHandler):
+    """Implements the OAuth 2.0 token exchange spec based on
+    https://tools.ietf.org/html/rfc8693.
+    """
+
+    def __init__(self, token_exchange_endpoint, client_authentication=None):
+        """Initializes an STS client instance.
+
+        Args:
+            token_exchange_endpoint (str): The token exchange endpoint.
+            client_authentication (Optional(google.oauth2.oauth2_utils.ClientAuthentication)):
+                The optional OAuth client authentication credentials if available.
+        """
+        super(Client, self).__init__(client_authentication)
+        self._token_exchange_endpoint = token_exchange_endpoint
+
+    def exchange_token(
+        self,
+        request,
+        grant_type,
+        subject_token,
+        subject_token_type,
+        resource=None,
+        audience=None,
+        scopes=None,
+        requested_token_type=None,
+        actor_token=None,
+        actor_token_type=None,
+        additional_options=None,
+        additional_headers=None,
+    ):
+        """Exchanges the provided token for another type of token based on the
+        rfc8693 spec.
+
+        Args:
+            request (google.auth.transport.Request): A callable used to make
+                HTTP requests.
+            grant_type (str): The OAuth 2.0 token exchange grant type.
+            subject_token (str): The OAuth 2.0 token exchange subject token.
+            subject_token_type (str): The OAuth 2.0 token exchange subject token type.
+            resource (Optional[str]): The optional OAuth 2.0 token exchange resource field.
+            audience (Optional[str]): The optional OAuth 2.0 token exchange audience field.
+            scopes (Optional[Sequence[str]]): The optional list of scopes to use.
+            requested_token_type (Optional[str]): The optional OAuth 2.0 token exchange requested
+                token type.
+            actor_token (Optional[str]): The optional OAuth 2.0 token exchange actor token.
+            actor_token_type (Optional[str]): The optional OAuth 2.0 token exchange actor token type.
+            additional_options (Optional[Mapping[str, str]]): The optional additional
+                non-standard Google specific options.
+            additional_headers (Optional[Mapping[str, str]]): The optional additional
+                headers to pass to the token exchange endpoint.
+
+        Returns:
+            Mapping[str, str]: The token exchange JSON-decoded response data containing
+                the requested token and its expiration time.
+
+        Raises:
+            google.auth.exceptions.OAuthError: If the token endpoint returned
+                an error.
+        """
+        # Initialize request headers.
+        headers = _URLENCODED_HEADERS.copy()
+        # Inject additional headers.
+        if additional_headers:
+            for k, v in dict(additional_headers).items():
+                headers[k] = v
+        # Initialize request body.
+        request_body = {
+            "grant_type": grant_type,
+            "resource": resource,
+            "audience": audience,
+            "scope": " ".join(scopes or []),
+            "requested_token_type": requested_token_type,
+            "subject_token": subject_token,
+            "subject_token_type": subject_token_type,
+            "actor_token": actor_token,
+            "actor_token_type": actor_token_type,
+            "options": None,
+        }
+        # Add additional non-standard options.
+        if additional_options:
+            request_body["options"] = urllib.parse.quote(json.dumps(additional_options))
+        # Remove empty fields in request body.
+        for k, v in dict(request_body).items():
+            if v is None or v == "":
+                del request_body[k]
+        # Apply OAuth client authentication.
+        self.apply_client_authentication_options(headers, request_body)
+
+        # Execute request.
+        response = request(
+            url=self._token_exchange_endpoint,
+            method="POST",
+            headers=headers,
+            body=urllib.parse.urlencode(request_body).encode("utf-8"),
+        )
+
+        response_body = (
+            response.data.decode("utf-8")
+            if hasattr(response.data, "decode")
+            else response.data
+        )
+
+        # If non-200 response received, translate to OAuthError exception.
+        if response.status != http_client.OK:
+            utils.handle_error_response(response_body)
+
+        response_data = json.loads(response_body)
+
+        # Return successful response.
+        return response_data
diff --git a/google/oauth2/utils.py b/google/oauth2/utils.py
new file mode 100644
index 0000000..593f032
--- /dev/null
+++ b/google/oauth2/utils.py
@@ -0,0 +1,171 @@
+# 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.
+
+"""OAuth 2.0 Utilities.
+
+This module provides implementations for various OAuth 2.0 utilities.
+This includes `OAuth error handling`_ and
+`Client authentication for OAuth flows`_.
+
+OAuth error handling
+--------------------
+This will define interfaces for handling OAuth related error responses as
+stated in `RFC 6749 section 5.2`_.
+This will include a common function to convert these HTTP error responses to a
+:class:`google.auth.exceptions.OAuthError` exception.
+
+
+Client authentication for OAuth flows
+-------------------------------------
+We introduce an interface for defining client authentication credentials based
+on `RFC 6749 section 2.3.1`_. This will expose the following
+capabilities:
+
+    * Ability to support basic authentication via request header.
+    * Ability to support bearer token authentication via request header.
+    * Ability to support client ID / secret authentication via request body.
+
+.. _RFC 6749 section 2.3.1: https://tools.ietf.org/html/rfc6749#section-2.3.1
+.. _RFC 6749 section 5.2: https://tools.ietf.org/html/rfc6749#section-5.2
+"""
+
+import abc
+import base64
+import enum
+import json
+
+import six
+
+from google.auth import exceptions
+
+
+# OAuth client authentication based on
+# https://tools.ietf.org/html/rfc6749#section-2.3.
+class ClientAuthType(enum.Enum):
+    basic = 1
+    request_body = 2
+
+
+class ClientAuthentication(object):
+    """Defines the client authentication credentials for basic and request-body
+    types based on https://tools.ietf.org/html/rfc6749#section-2.3.1.
+    """
+
+    def __init__(self, client_auth_type, client_id, client_secret=None):
+        """Instantiates a client authentication object containing the client ID
+        and secret credentials for basic and response-body auth.
+
+        Args:
+            client_auth_type (google.oauth2.oauth_utils.ClientAuthType): The
+                client authentication type.
+            client_id (str): The client ID.
+            client_secret (Optional[str]): The client secret.
+        """
+        self.client_auth_type = client_auth_type
+        self.client_id = client_id
+        self.client_secret = client_secret
+
+
[email protected]_metaclass(abc.ABCMeta)
+class OAuthClientAuthHandler(object):
+    """Abstract class for handling client authentication in OAuth-based
+    operations.
+    """
+
+    def __init__(self, client_authentication=None):
+        """Instantiates an OAuth client authentication handler.
+
+        Args:
+            client_authentication (Optional[google.oauth2.utils.ClientAuthentication]):
+                The OAuth client authentication credentials if available.
+        """
+        super(OAuthClientAuthHandler, self).__init__()
+        self._client_authentication = client_authentication
+
+    def apply_client_authentication_options(
+        self, headers, request_body=None, bearer_token=None
+    ):
+        """Applies client authentication on the OAuth request's headers or POST
+        body.
+
+        Args:
+            headers (Mapping[str, str]): The HTTP request header.
+            request_body (Optional[Mapping[str, str]]): The HTTP request body
+                dictionary. For requests that do not support request body, this
+                is None and will be ignored.
+            bearer_token (Optional[str]): The optional bearer token.
+        """
+        # Inject authenticated header.
+        self._inject_authenticated_headers(headers, bearer_token)
+        # Inject authenticated request body.
+        if bearer_token is None:
+            self._inject_authenticated_request_body(request_body)
+
+    def _inject_authenticated_headers(self, headers, bearer_token=None):
+        if bearer_token is not None:
+            headers["Authorization"] = "Bearer %s" % bearer_token
+        elif (
+            self._client_authentication is not None
+            and self._client_authentication.client_auth_type is ClientAuthType.basic
+        ):
+            username = self._client_authentication.client_id
+            password = self._client_authentication.client_secret or ""
+
+            credentials = base64.b64encode(
+                ("%s:%s" % (username, password)).encode()
+            ).decode()
+            headers["Authorization"] = "Basic %s" % credentials
+
+    def _inject_authenticated_request_body(self, request_body):
+        if (
+            self._client_authentication is not None
+            and self._client_authentication.client_auth_type
+            is ClientAuthType.request_body
+        ):
+            if request_body is None:
+                raise exceptions.OAuthError(
+                    "HTTP request does not support request-body"
+                )
+            else:
+                request_body["client_id"] = self._client_authentication.client_id
+                request_body["client_secret"] = (
+                    self._client_authentication.client_secret or ""
+                )
+
+
+def handle_error_response(response_body):
+    """Translates an error response from an OAuth operation into an
+    OAuthError exception.
+
+    Args:
+        response_body (str): The decoded response data.
+
+    Raises:
+        google.auth.exceptions.OAuthError
+    """
+    try:
+        error_components = []
+        error_data = json.loads(response_body)
+
+        error_components.append("Error code {}".format(error_data["error"]))
+        if "error_description" in error_data:
+            error_components.append(": {}".format(error_data["error_description"]))
+        if "error_uri" in error_data:
+            error_components.append(" - {}".format(error_data["error_uri"]))
+        error_details = "".join(error_components)
+    # If no details could be extracted, use the response data.
+    except (KeyError, ValueError):
+        error_details = response_body
+
+    raise exceptions.OAuthError(error_details, response_body)
diff --git a/noxfile.py b/noxfile.py
new file mode 100644
index 0000000..efb367e
--- /dev/null
+++ b/noxfile.py
@@ -0,0 +1,169 @@
+# Copyright 2019 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 os
+import pathlib
+import shutil
+
+import nox
+
+CURRENT_DIRECTORY = pathlib.Path(__file__).parent.absolute()
+
+BLACK_VERSION = "black==19.3b0"
+BLACK_PATHS = [
+    "google",
+    "tests",
+    "tests_async",
+    "noxfile.py",
+    "setup.py",
+    "docs/conf.py",
+]
+
+
[email protected](python="3.7")
+def lint(session):
+    session.install("flake8", "flake8-import-order", "docutils", BLACK_VERSION)
+    session.install("-e", ".")
+    session.run("black", "--check", *BLACK_PATHS)
+    session.run(
+        "flake8",
+        "--import-order-style=google",
+        "--application-import-names=google,tests,system_tests",
+        "google",
+        "tests",
+        "tests_async",
+    )
+    session.run(
+        "python", "setup.py", "check", "--metadata", "--restructuredtext", "--strict"
+    )
+
+
[email protected](python="3.8")
+def blacken(session):
+    """Run black.
+    Format code to uniform standard.
+    The Python version should be consistent with what is
+    supplied in the Python Owlbot postprocessor.
+
+    https://github.com/googleapis/synthtool/blob/master/docker/owlbot/python/Dockerfile
+    """
+    session.install(BLACK_VERSION)
+    session.run("black", *BLACK_PATHS)
+
+
[email protected](python=["3.6", "3.7", "3.8", "3.9", "3.10"])
+def unit(session):
+    constraints_path = str(
+        CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt"
+    )
+    session.install("-r", "testing/requirements.txt", "-c", constraints_path)
+    session.install("-e", ".", "-c", constraints_path)
+    session.run(
+        "pytest",
+        f"--junitxml=unit_{session.python}_sponge_log.xml",
+        "--cov=google.auth",
+        "--cov=google.oauth2",
+        "--cov=tests",
+        "--cov-report=term-missing",
+        "tests",
+        "tests_async",
+    )
+
+
[email protected](python=["2.7"])
+def unit_prev_versions(session):
+    constraints_path = str(
+        CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt"
+    )
+    session.install("-r", "testing/requirements.txt", "-c", constraints_path)
+    session.install("-e", ".", "-c", constraints_path)
+    session.run(
+        "pytest",
+        f"--junitxml=unit_{session.python}_sponge_log.xml",
+        "--cov=google.auth",
+        "--cov=google.oauth2",
+        "--cov=tests",
+        "tests",
+    )
+
+
[email protected](python="3.7")
+def cover(session):
+    session.install("-r", "testing/requirements.txt")
+    session.install("-e", ".")
+    session.run(
+        "pytest",
+        "--cov=google.auth",
+        "--cov=google.oauth2",
+        "--cov=tests",
+        "--cov=tests_async",
+        "--cov-report=term-missing",
+        "tests",
+        "tests_async",
+    )
+    session.run("coverage", "report", "--show-missing", "--fail-under=100")
+
+
[email protected](python="3.7")
+def docgen(session):
+    session.env["SPHINX_APIDOC_OPTIONS"] = "members,inherited-members,show-inheritance"
+    session.install("-r", "testing/requirements.txt")
+    session.install("sphinx")
+    session.install("-e", ".")
+    session.run("rm", "-r", "docs/reference")
+    session.run(
+        "sphinx-apidoc",
+        "--output-dir",
+        "docs/reference",
+        "--separate",
+        "--module-first",
+        "google",
+    )
+
+
[email protected](python="3.8")
+def docs(session):
+    """Build the docs for this library."""
+
+    session.install("-e", ".[aiohttp]")
+    session.install("sphinx", "alabaster", "recommonmark", "sphinx-docstring-typing")
+
+    shutil.rmtree(os.path.join("docs", "_build"), ignore_errors=True)
+    session.run(
+        "sphinx-build",
+        "-T",  # show full traceback on exception
+        "-W",  # warnings as errors
+        "-N",  # no colors
+        "-b",
+        "html",
+        "-d",
+        os.path.join("docs", "_build", "doctrees", ""),
+        os.path.join("docs", ""),
+        os.path.join("docs", "_build", "html", ""),
+    )
+
+
[email protected](python="pypy")
+def pypy(session):
+    session.install("-r", "test/requirements.txt")
+    session.install("-e", ".")
+    session.run(
+        "pytest",
+        f"--junitxml=unit_{session.python}_sponge_log.xml",
+        "--cov=google.auth",
+        "--cov=google.oauth2",
+        "--cov=tests",
+        "tests",
+        "tests_async",
+    )
diff --git a/owlbot.py b/owlbot.py
new file mode 100644
index 0000000..611ce92
--- /dev/null
+++ b/owlbot.py
@@ -0,0 +1,32 @@
+import synthtool as s
+from synthtool import gcp
+
+common = gcp.CommonTemplates()
+
+# ----------------------------------------------------------------------------
+# Add templated files
+# ----------------------------------------------------------------------------
+templated_files = common.py_library(unit_cov_level=100, cov_level=100)
+
+
+s.move(
+    templated_files / ".kokoro",
+    excludes=[
+        "continuous/common.cfg",
+        "presubmit/common.cfg",
+        "build.sh",
+    ],
+)  # just move kokoro configs
+s.move(
+    # needed by samples kokoro jobs
+    templated_files / ".trampolinerc"
+)
+
+
+assert 1 == s.replace(
+    ".kokoro/docs/docs-presubmit.cfg",
+    'value: "docs docfx"',
+    'value: "docs"',
+)
+
+s.shell.run(["nox", "-s", "blacken"], hide_output=False)
diff --git a/renovate.json b/renovate.json
new file mode 100644
index 0000000..4fa9493
--- /dev/null
+++ b/renovate.json
@@ -0,0 +1,5 @@
+{
+  "extends": [
+    "config:base",  ":preserveSemverRanges"
+  ]
+}
diff --git a/scripts/decrypt-secrets.sh b/scripts/decrypt-secrets.sh
new file mode 100755
index 0000000..f0ef994
--- /dev/null
+++ b/scripts/decrypt-secrets.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# 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.
+
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+ROOT=$( dirname "$DIR" )
+
+# Work from the project root.
+cd $ROOT
+
+gcloud kms decrypt \
+  --location=global \
+  --keyring=ci \
+  --key=kokoro-secrets \
+  --ciphertext-file=system_tests/secrets.tar.enc \
+  --plaintext-file=system_tests/secrets.tar
+tar xvf system_tests/secrets.tar
+rm system_tests/secrets.tar
diff --git a/scripts/encrypt-secrets.sh b/scripts/encrypt-secrets.sh
new file mode 100755
index 0000000..b6521e8
--- /dev/null
+++ b/scripts/encrypt-secrets.sh
@@ -0,0 +1,32 @@
+#!/bin/bash
+
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# 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.
+
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+ROOT=$( dirname "$DIR" )
+
+# Work from the project root.
+cd $ROOT
+
+tar cvf system_tests/secrets.tar system_tests/data
+
+gcloud kms encrypt \
+  --location=global  \
+  --keyring=ci \
+  --key=kokoro-secrets \
+  --plaintext-file=system_tests/secrets.tar \
+  --ciphertext-file=system_tests/secrets.tar.enc
+
+rm system_tests/secrets.tar
\ No newline at end of file
diff --git a/scripts/setup_external_accounts.sh b/scripts/setup_external_accounts.sh
new file mode 100644
index 0000000..ecc879b
--- /dev/null
+++ b/scripts/setup_external_accounts.sh
@@ -0,0 +1,113 @@
+#!/bin/bash
+# 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.
+
+# This file is a mostly common setup file to ensure all workload identity
+# federation integration tests are set up in a consistent fashion across the
+# languages in our various client libraries. It assumes that the current user
+# has the relevant permissions to run each of the commands listed.
+
+# This script needs to be run once. It will do the following:
+# 1. Create a random workload identity pool.
+# 2. Create a random OIDC provider in that pool which uses the
+#    accounts.google.com as the issuer and the default STS audience as the
+#    allowed audience. This audience will be validated on STS token exchange.
+# 3. Enable OIDC tokens generated by the current service account to impersonate
+#    the service account. (Identified by the OIDC token sub field which is the
+#    service account client ID).
+# 4. Create a random AWS provider in that pool which uses the provided AWS
+#    account ID.
+# 5. Enable AWS provider to impersonate the service account. (Principal is
+#    identified by the AWS role name).
+# 6. Print out the STS audience fields associated with the created providers
+#    after the setup completes successfully so that they can be used in the
+#    tests. These will be copied and used as the global _AUDIENCE_OIDC and
+#    _AUDIENCE_AWS constants in system_tests/system_tests_sync/test_external_accounts.py.
+#
+# It is safe to run the setup script again. A new pool is created and new
+# audiences are printed. If run multiple times, it is advisable to delete
+# unused pools. Note that deleted pools are soft deleted and may remain for
+# a while before they are completely deleted. The old pool ID cannot be used
+# in the meantime.
+#
+# For AWS tests, an AWS developer account is needed.
+# The following AWS prerequisite setup is needed.
+# 1. An OIDC Google identity provider needs to be created with the following:
+#    issuer: accounts.google.com
+#    audience: Use the client_id of the service account.
+# 2. A role for OIDC web identity federation is needed with the created Google
+#    provider as a trusted entity:
+#    "accounts.google.com:aud": "$CLIENT_ID"
+# The steps are documented at:
+# https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-idp_oidc.html
+
+suffix=""
+
+function generate_random_string () {
+  local valid_chars=abcdefghijklmnopqrstuvwxyz0123456789
+  for i in {1..8} ; do
+    suffix+="${valid_chars:RANDOM%${#valid_chars}:1}"
+    done
+}
+
+generate_random_string
+
+pool_id="pool-"$suffix
+oidc_provider_id="oidc-"$suffix
+aws_provider_id="aws-"$suffix
+
+# TODO: Fill in.
+project_id="stellar-day-254222"
+project_number="79992041559"
+aws_account_id="077071391996"
+aws_role_name="ci-python-test"
+service_account_email="[email protected]"
+sub="104692443208068386138"
+
+oidc_aud="//iam.googleapis.com/projects/$project_number/locations/global/workloadIdentityPools/$pool_id/providers/$oidc_provider_id"
+aws_aud="//iam.googleapis.com/projects/$project_number/locations/global/workloadIdentityPools/$pool_id/providers/$aws_provider_id"
+
+gcloud config set project $project_id
+
+# Create the Workload Identity Pool.
+gcloud beta iam workload-identity-pools create $pool_id \
+    --location="global" \
+    --description="Test pool" \
+    --display-name="Test pool for Python"
+
+# Create the OIDC Provider.
+gcloud beta iam workload-identity-pools providers create-oidc $oidc_provider_id \
+    --workload-identity-pool=$pool_id \
+    --issuer-uri="https://accounts.google.com" \
+    --location="global" \
+    --attribute-mapping="google.subject=assertion.sub"
+
+# Create the AWS Provider.
+gcloud beta iam workload-identity-pools providers create-aws $aws_provider_id \
+    --workload-identity-pool=$pool_id \
+    --account-id=$aws_account_id \
+    --location="global"
+
+# Give permission to impersonate the service account.
+gcloud iam service-accounts add-iam-policy-binding $service_account_email \
+--role roles/iam.workloadIdentityUser \
+--member "principal://iam.googleapis.com/projects/$project_number/locations/global/workloadIdentityPools/$pool_id/subject/$sub"
+
+gcloud iam service-accounts add-iam-policy-binding $service_account_email \
+  --role roles/iam.workloadIdentityUser \
+  --member "principalSet://iam.googleapis.com/projects/$project_number/locations/global/workloadIdentityPools/$pool_id/attribute.aws_role/arn:aws:sts::$aws_account_id:assumed-role/$aws_role_name"
+
+echo "OIDC audience: "$oidc_aud
+echo "AWS audience: "$aws_aud
+echo "AWS role: arn:aws:iam::$aws_account_id:role/$aws_role_name"
diff --git a/scripts/travis.sh b/scripts/travis.sh
new file mode 100755
index 0000000..2c34091
--- /dev/null
+++ b/scripts/travis.sh
@@ -0,0 +1,42 @@
+#!/bin/bash
+
+# Copyright 2015 Google Inc. All rights reserved.
+#
+# 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.
+
+set -eo pipefail
+
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+ROOT=$( dirname "$DIR" )
+
+# Work from the project root.
+cd $ROOT
+
+# Decrypt secrets and run system tests if not on an external PR.
+if [[ -n $SYSTEM_TEST ]]; then
+    if [[ $TRAVIS_SECURE_ENV_VARS == "true" ]]; then
+        echo 'Extracting secrets.'
+        scripts/decrypt-secrets.sh "$SECRETS_PASSWORD"
+        # Prevent build failures from leaking our password.
+        # looking at you, Tox.
+        export SECRETS_PASSWORD=""
+    else
+        # This is an external PR, so just mark system tests as green.
+        echo 'In system test but secrets are not available, skipping.'
+        exit 0
+    fi
+fi
+
+# Run nox.
+echo "Running nox..."
+nox
\ No newline at end of file
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..7c2b287
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,2 @@
+[bdist_wheel]
+universal = 1
\ No newline at end of file
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..301e996
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,85 @@
+# Copyright 2014 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import io
+import os
+
+from setuptools import find_packages
+from setuptools import setup
+
+
+DEPENDENCIES = (
+    "cachetools>=2.0.0,<5.0",
+    "pyasn1-modules>=0.2.1",
+    # rsa==4.5 is the last version to support 2.7
+    # https://github.com/sybrenstuvel/python-rsa/issues/152#issuecomment-643470233
+    'rsa<4.6; python_version < "3.6"',
+    'rsa>=3.1.4,<5; python_version >= "3.6"',
+    # install enum34 to support 2.7. enum34 only works up to python version 3.3.
+    'enum34>=1.1.10; python_version < "3.4"',
+    "setuptools>=40.3.0",
+    "six>=1.9.0",
+)
+
+extras = {
+    "aiohttp": [
+        "aiohttp >= 3.6.2, < 4.0.0dev; python_version>='3.6'",
+        "requests >= 2.20.0, < 3.0.0dev",
+    ],
+    "pyopenssl": "pyopenssl>=20.0.0",
+    "reauth": "pyu2f>=0.1.5",
+}
+
+with io.open("README.rst", "r") as fh:
+    long_description = fh.read()
+
+package_root = os.path.abspath(os.path.dirname(__file__))
+
+version = {}
+with open(os.path.join(package_root, "google/auth/version.py")) as fp:
+    exec(fp.read(), version)
+version = version["__version__"]
+
+setup(
+    name="google-auth",
+    version=version,
+    author="Google Cloud Platform",
+    author_email="[email protected]",
+    description="Google Authentication Library",
+    long_description=long_description,
+    url="https://github.com/googleapis/google-auth-library-python",
+    packages=find_packages(exclude=("tests*", "system_tests*")),
+    namespace_packages=("google",),
+    install_requires=DEPENDENCIES,
+    extras_require=extras,
+    python_requires=">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*",
+    license="Apache 2.0",
+    keywords="google auth oauth client",
+    classifiers=[
+        "Programming Language :: Python :: 3",
+        "Programming Language :: Python :: 3.6",
+        "Programming Language :: Python :: 3.7",
+        "Programming Language :: Python :: 3.8",
+        "Programming Language :: Python :: 3.9",
+        "Programming Language :: Python :: 3.10",
+        "Development Status :: 5 - Production/Stable",
+        "Intended Audience :: Developers",
+        "License :: OSI Approved :: Apache Software License",
+        "Operating System :: POSIX",
+        "Operating System :: Microsoft :: Windows",
+        "Operating System :: MacOS :: MacOS X",
+        "Operating System :: OS Independent",
+        "Topic :: Internet :: WWW/HTTP",
+    ],
+)
diff --git a/system_tests/__init__.py b/system_tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/system_tests/__init__.py
diff --git a/system_tests/noxfile.py b/system_tests/noxfile.py
new file mode 100644
index 0000000..459b71c
--- /dev/null
+++ b/system_tests/noxfile.py
@@ -0,0 +1,498 @@
+# 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.
+
+"""Noxfile for automating system tests.
+
+This file handles setting up environments needed by the system tests. This
+separates the tests from their environment configuration.
+
+See the `nox docs`_ for details on how this file works:
+
+.. _nox docs: http://nox.readthedocs.io/en/latest/
+"""
+
+import os
+import subprocess
+
+from nox.command import which
+import nox
+import py.path
+
+HERE = os.path.abspath(os.path.dirname(__file__))
+LIBRARY_DIR = os.path.abspath(os.path.dirname(HERE))
+DATA_DIR = os.path.join(HERE, "data")
+SERVICE_ACCOUNT_FILE = os.path.join(DATA_DIR, "service_account.json")
+AUTHORIZED_USER_FILE = os.path.join(DATA_DIR, "authorized_user.json")
+EXPLICIT_CREDENTIALS_ENV = "GOOGLE_APPLICATION_CREDENTIALS"
+EXPLICIT_PROJECT_ENV = "GOOGLE_CLOUD_PROJECT"
+EXPECT_PROJECT_ENV = "EXPECT_PROJECT_ID"
+
+SKIP_GAE_TEST_ENV = "SKIP_APP_ENGINE_SYSTEM_TEST"
+GAE_APP_URL_TMPL = "https://{}-dot-{}.appspot.com"
+GAE_TEST_APP_SERVICE = "google-auth-system-tests"
+
+# The download location for the Cloud SDK
+CLOUD_SDK_DIST_FILENAME = "google-cloud-sdk.tar.gz"
+CLOUD_SDK_DOWNLOAD_URL = "https://dl.google.com/dl/cloudsdk/release/{}".format(
+    CLOUD_SDK_DIST_FILENAME
+)
+
+# This environment variable is recognized by the Cloud SDK and overrides
+# the location of the SDK's configuration files (which is usually at
+# ${HOME}/.config).
+CLOUD_SDK_CONFIG_ENV = "CLOUDSDK_CONFIG"
+
+# If set, this is where the environment setup will install the Cloud SDK.
+# If unset, it will download the SDK to a temporary directory.
+CLOUD_SDK_ROOT = os.environ.get("CLOUD_SDK_ROOT")
+
+if CLOUD_SDK_ROOT is not None:
+    CLOUD_SDK_ROOT = py.path.local(CLOUD_SDK_ROOT)
+    CLOUD_SDK_ROOT.ensure(dir=True)  # Makes sure the directory exists.
+else:
+    CLOUD_SDK_ROOT = py.path.local.mkdtemp()
+
+# The full path the cloud sdk install directory
+CLOUD_SDK_INSTALL_DIR = CLOUD_SDK_ROOT.join("google-cloud-sdk")
+
+# The full path to the gcloud cli executable.
+GCLOUD = str(CLOUD_SDK_INSTALL_DIR.join("bin", "gcloud"))
+
+# gcloud requires Python 2 and doesn't work on 3, so we need to tell it
+# where to find 2 when we're running in a 3 environment.
+CLOUD_SDK_PYTHON_ENV = "CLOUDSDK_PYTHON"
+CLOUD_SDK_PYTHON = which("python2", None)
+
+# Cloud SDK helpers
+
+
+def install_cloud_sdk(session):
+    """Downloads and installs the Google Cloud SDK."""
+    # Configure environment variables needed by the SDK.
+    # This sets the config root to the tests' config root. This prevents
+    # our tests from clobbering a developer's configuration when running
+    # these tests locally.
+    session.env[CLOUD_SDK_CONFIG_ENV] = str(CLOUD_SDK_ROOT)
+    # This tells gcloud which Python interpreter to use (always use 2.7)
+    session.env[CLOUD_SDK_PYTHON_ENV] = CLOUD_SDK_PYTHON
+    # This set the $PATH for the subprocesses so they can find the gcloud
+    # executable.
+    session.env["PATH"] = (
+        str(CLOUD_SDK_INSTALL_DIR.join("bin")) + os.pathsep + os.environ["PATH"]
+    )
+
+    # If gcloud cli executable already exists, just update it.
+    if py.path.local(GCLOUD).exists():
+        session.run(GCLOUD, "components", "update", "-q")
+        return
+
+    tar_path = CLOUD_SDK_ROOT.join(CLOUD_SDK_DIST_FILENAME)
+
+    # Download the release.
+    session.run("wget", CLOUD_SDK_DOWNLOAD_URL, "-O", str(tar_path), silent=True)
+
+    # Extract the release.
+    session.run("tar", "xzf", str(tar_path), "-C", str(CLOUD_SDK_ROOT))
+    session.run(tar_path.remove)
+
+    # Run the install script.
+    session.run(
+        str(CLOUD_SDK_INSTALL_DIR.join("install.sh")),
+        "--usage-reporting",
+        "false",
+        "--path-update",
+        "false",
+        "--command-completion",
+        "false",
+        silent=True,
+    )
+
+
+def copy_credentials(credentials_path):
+    """Copies credentials into the SDK root as the application default
+    credentials."""
+    dest = CLOUD_SDK_ROOT.join("application_default_credentials.json")
+    if dest.exists():
+        dest.remove()
+    py.path.local(credentials_path).copy(dest)
+
+
+def configure_cloud_sdk(session, application_default_credentials, project=False):
+    """Installs and configures the Cloud SDK with the given application default
+    credentials.
+
+    If project is True, then a project will be set in the active config.
+    If it is false, this will ensure no project is set.
+    """
+    install_cloud_sdk(session)
+
+    # Setup the service account as the default user account. This is
+    # needed for the project ID detection to work. Note that this doesn't
+    # change the application default credentials file, which is user
+    # credentials instead of service account credentials sometimes.
+    session.run(
+        GCLOUD, "auth", "activate-service-account", "--key-file", SERVICE_ACCOUNT_FILE
+    )
+
+    if project:
+        session.run(GCLOUD, "config", "set", "project", "example-project")
+    else:
+        session.run(GCLOUD, "config", "unset", "project")
+
+    # Copy the credentials file to the config root. This is needed because
+    # unfortunately gcloud doesn't provide a clean way to tell it to use
+    # a particular set of credentials. However, this does verify that gcloud
+    # also considers the credentials valid by calling application-default
+    # print-access-token
+    session.run(copy_credentials, application_default_credentials)
+
+    # Calling this forces the Cloud SDK to read the credentials we just wrote
+    # and obtain a new access token with those credentials. This validates
+    # that our credentials matches the format expected by gcloud.
+    # Silent is set to True to prevent leaking secrets in test logs.
+    session.run(
+        GCLOUD, "auth", "application-default", "print-access-token", silent=True
+    )
+
+
+# Test sesssions
+
+TEST_DEPENDENCIES_ASYNC = ["aiohttp", "pytest-asyncio", "nest-asyncio"]
+TEST_DEPENDENCIES_SYNC = ["pytest", "requests", "mock"]
+PYTHON_VERSIONS_ASYNC = ["3.7"]
+PYTHON_VERSIONS_SYNC = ["2.7", "3.7"]
+
+
+def default(session, *test_paths):
+    # replace 'session._runner.friendly_name' with
+    # session.name once nox has released a new version
+    # https://github.com/theacodes/nox/pull/386
+    sponge_log = f"--junitxml=system_{str(session._runner.friendly_name)}_sponge_log.xml"
+    session.run(
+        "pytest", sponge_log, *test_paths,
+    )
+
+
[email protected](python=PYTHON_VERSIONS_SYNC)
+def service_account_sync(session):
+    session.install(*TEST_DEPENDENCIES_SYNC)
+    session.install(LIBRARY_DIR)
+    default(
+        session,
+        "system_tests_sync/test_service_account.py",
+        *session.posargs,
+    )
+
+
[email protected](python=PYTHON_VERSIONS_SYNC)
+def default_explicit_service_account(session):
+    session.env[EXPLICIT_CREDENTIALS_ENV] = SERVICE_ACCOUNT_FILE
+    session.env[EXPECT_PROJECT_ENV] = "1"
+    session.install(*TEST_DEPENDENCIES_SYNC)
+    session.install(LIBRARY_DIR)
+    default(
+        session,
+        "system_tests_sync/test_default.py",
+        "system_tests_sync/test_id_token.py",
+        *session.posargs,
+    )
+
+
[email protected](python=PYTHON_VERSIONS_SYNC)
+def default_explicit_authorized_user(session):
+    session.env[EXPLICIT_CREDENTIALS_ENV] = AUTHORIZED_USER_FILE
+    session.install(*TEST_DEPENDENCIES_SYNC)
+    session.install(LIBRARY_DIR)
+    default(
+        session,
+        "system_tests_sync/test_default.py",
+        *session.posargs,
+    )
+
+
[email protected](python=PYTHON_VERSIONS_SYNC)
+def default_explicit_authorized_user_explicit_project(session):
+    session.env[EXPLICIT_CREDENTIALS_ENV] = AUTHORIZED_USER_FILE
+    session.env[EXPLICIT_PROJECT_ENV] = "example-project"
+    session.env[EXPECT_PROJECT_ENV] = "1"
+    session.install(*TEST_DEPENDENCIES_SYNC)
+    session.install(LIBRARY_DIR)
+    default(
+        session,
+        "system_tests_sync/test_default.py",
+        *session.posargs,
+    )
+
+
[email protected](python=PYTHON_VERSIONS_SYNC)
+def default_cloud_sdk_service_account(session):
+    configure_cloud_sdk(session, SERVICE_ACCOUNT_FILE)
+    session.env[EXPECT_PROJECT_ENV] = "1"
+    session.install(*TEST_DEPENDENCIES_SYNC)
+    session.install(LIBRARY_DIR)
+    default(
+        session,
+        "system_tests_sync/test_default.py",
+        *session.posargs,
+    )
+
+
[email protected](python=PYTHON_VERSIONS_SYNC)
+def default_cloud_sdk_authorized_user(session):
+    configure_cloud_sdk(session, AUTHORIZED_USER_FILE)
+    session.install(*TEST_DEPENDENCIES_SYNC)
+    session.install(LIBRARY_DIR)
+    default(
+        session,
+        "system_tests_sync/test_default.py",
+        *session.posargs,
+    )
+
+
[email protected](python=PYTHON_VERSIONS_SYNC)
+def default_cloud_sdk_authorized_user_configured_project(session):
+    configure_cloud_sdk(session, AUTHORIZED_USER_FILE, project=True)
+    session.env[EXPECT_PROJECT_ENV] = "1"
+    session.install(*TEST_DEPENDENCIES_SYNC)
+    session.install(LIBRARY_DIR)
+    default(
+        session,
+        "system_tests_sync/test_default.py",
+        *session.posargs,
+    )
+
+
[email protected](python=PYTHON_VERSIONS_SYNC)
+def compute_engine(session):
+    session.install(*TEST_DEPENDENCIES_SYNC)
+    # unset Application Default Credentials so
+    # credentials are detected from environment
+    del session.virtualenv.env["GOOGLE_APPLICATION_CREDENTIALS"]
+    session.install(LIBRARY_DIR)
+    default(
+        session,
+        "system_tests_sync/test_compute_engine.py",
+        *session.posargs,
+    )
+
+
[email protected](python=["2.7"])
+def app_engine(session):
+    if SKIP_GAE_TEST_ENV in os.environ:
+        session.log("Skipping App Engine tests.")
+        return
+
+    session.install(LIBRARY_DIR)
+    # Unlike the default tests above, the App Engine system test require a
+    # 'real' gcloud sdk installation that is configured to deploy to an
+    # app engine project.
+    # Grab the project ID from the cloud sdk.
+    project_id = (
+        subprocess.check_output(
+            ["gcloud", "config", "list", "project", "--format", "value(core.project)"]
+        )
+        .decode("utf-8")
+        .strip()
+    )
+
+    if not project_id:
+        session.error(
+            "The Cloud SDK must be installed and configured to deploy to App " "Engine."
+        )
+
+    application_url = GAE_APP_URL_TMPL.format(GAE_TEST_APP_SERVICE, project_id)
+
+    # Vendor in the test application's dependencies
+    session.chdir(os.path.join(HERE, "system_tests_sync/app_engine_test_app"))
+    session.install(*TEST_DEPENDENCIES_SYNC)
+    session.run(
+        "pip", "install", "--target", "lib", "-r", "requirements.txt", silent=True
+    )
+
+    # Deploy the application.
+    session.run("gcloud", "app", "deploy", "-q", "app.yaml")
+
+    # Run the tests
+    session.env["TEST_APP_URL"] = application_url
+    session.chdir(HERE)
+    default(
+        session, "system_tests_sync/test_app_engine.py",
+    )
+
+
[email protected](python=PYTHON_VERSIONS_SYNC)
+def grpc(session):
+    session.install(LIBRARY_DIR)
+    session.install(*TEST_DEPENDENCIES_SYNC, "google-cloud-pubsub==1.7.0")
+    session.env[EXPLICIT_CREDENTIALS_ENV] = SERVICE_ACCOUNT_FILE
+    default(
+        session,
+        "system_tests_sync/test_grpc.py",
+        *session.posargs,
+    )
+
+
[email protected](python=PYTHON_VERSIONS_SYNC)
+def requests(session):
+    session.install(LIBRARY_DIR)
+    session.install(*TEST_DEPENDENCIES_SYNC)
+    session.env[EXPLICIT_CREDENTIALS_ENV] = SERVICE_ACCOUNT_FILE
+    default(
+        session,
+        "system_tests_sync/test_requests.py",
+        *session.posargs,
+    )
+
+
[email protected](python=PYTHON_VERSIONS_SYNC)
+def urllib3(session):
+    session.install(LIBRARY_DIR)
+    session.install(*TEST_DEPENDENCIES_SYNC)
+    session.env[EXPLICIT_CREDENTIALS_ENV] = SERVICE_ACCOUNT_FILE
+    default(
+        session,
+        "system_tests_sync/test_urllib3.py",
+        *session.posargs,
+    )
+
+
[email protected](python=PYTHON_VERSIONS_SYNC)
+def mtls_http(session):
+    session.install(LIBRARY_DIR)
+    session.install(*TEST_DEPENDENCIES_SYNC, "pyopenssl")
+    session.env[EXPLICIT_CREDENTIALS_ENV] = SERVICE_ACCOUNT_FILE
+    default(
+        session,
+        "system_tests_sync/test_mtls_http.py",
+        *session.posargs,
+    )
+
+
[email protected](python=PYTHON_VERSIONS_SYNC)
+def external_accounts(session):
+    session.install(
+        *TEST_DEPENDENCIES_SYNC,
+        LIBRARY_DIR,
+        "google-api-python-client",
+    )
+    default(
+        session,
+        "system_tests_sync/test_external_accounts.py",
+        *session.posargs,
+    )
+
+
[email protected](python=PYTHON_VERSIONS_SYNC)
+def downscoping(session):
+    session.install(
+        *TEST_DEPENDENCIES_SYNC,
+        LIBRARY_DIR,
+        "google-cloud-storage",
+    )
+    default(
+        session,
+        "system_tests_sync/test_downscoping.py",
+        *session.posargs,
+    )
+
+
+# ASYNC SYSTEM TESTS
+
[email protected](python=PYTHON_VERSIONS_ASYNC)
+def service_account_async(session):
+    session.install(*(TEST_DEPENDENCIES_SYNC + TEST_DEPENDENCIES_ASYNC))
+    session.install(LIBRARY_DIR)
+    default(
+        session,
+        "system_tests_async/test_service_account.py",
+        *session.posargs,
+    )
+
+
[email protected](python=PYTHON_VERSIONS_ASYNC)
+def default_explicit_service_account_async(session):
+    session.env[EXPLICIT_CREDENTIALS_ENV] = SERVICE_ACCOUNT_FILE
+    session.env[EXPECT_PROJECT_ENV] = "1"
+    session.install(*(TEST_DEPENDENCIES_SYNC + TEST_DEPENDENCIES_ASYNC))
+    session.install(LIBRARY_DIR)
+    default(
+        session,
+        "system_tests_async/test_default.py",
+        "system_tests_async/test_id_token.py",
+        *session.posargs,
+    )
+
+
[email protected](python=PYTHON_VERSIONS_ASYNC)
+def default_explicit_authorized_user_async(session):
+    session.env[EXPLICIT_CREDENTIALS_ENV] = AUTHORIZED_USER_FILE
+    session.install(*(TEST_DEPENDENCIES_SYNC + TEST_DEPENDENCIES_ASYNC))
+    session.install(LIBRARY_DIR)
+    default(
+        session,
+        "system_tests_async/test_default.py",
+        *session.posargs,
+    )
+
+
[email protected](python=PYTHON_VERSIONS_ASYNC)
+def default_explicit_authorized_user_explicit_project_async(session):
+    session.env[EXPLICIT_CREDENTIALS_ENV] = AUTHORIZED_USER_FILE
+    session.env[EXPLICIT_PROJECT_ENV] = "example-project"
+    session.env[EXPECT_PROJECT_ENV] = "1"
+    session.install(*(TEST_DEPENDENCIES_SYNC + TEST_DEPENDENCIES_ASYNC))
+    session.install(LIBRARY_DIR)
+    default(
+        session,
+        "system_tests_async/test_default.py",
+        *session.posargs,
+    )
+
+
[email protected](python=PYTHON_VERSIONS_ASYNC)
+def default_cloud_sdk_service_account_async(session):
+    configure_cloud_sdk(session, SERVICE_ACCOUNT_FILE)
+    session.env[EXPECT_PROJECT_ENV] = "1"
+    session.install(*(TEST_DEPENDENCIES_SYNC + TEST_DEPENDENCIES_ASYNC))
+    session.install(LIBRARY_DIR)
+    default(
+        session,
+        "system_tests_async/test_default.py",
+        *session.posargs,
+    )
+
+
[email protected](python=PYTHON_VERSIONS_ASYNC)
+def default_cloud_sdk_authorized_user_async(session):
+    configure_cloud_sdk(session, AUTHORIZED_USER_FILE)
+    session.install(*(TEST_DEPENDENCIES_SYNC + TEST_DEPENDENCIES_ASYNC))
+    session.install(LIBRARY_DIR)
+    default(
+        session,
+        "system_tests_async/test_default.py",
+        *session.posargs,
+    )
+
+
[email protected](python=PYTHON_VERSIONS_ASYNC)
+def default_cloud_sdk_authorized_user_configured_project_async(session):
+    configure_cloud_sdk(session, AUTHORIZED_USER_FILE, project=True)
+    session.env[EXPECT_PROJECT_ENV] = "1"
+    session.install(*(TEST_DEPENDENCIES_SYNC + TEST_DEPENDENCIES_ASYNC))
+    session.install(LIBRARY_DIR)
+    default(
+        session,
+        "system_tests_async/test_default.py",
+        *session.posargs,
+    )
diff --git a/system_tests/secrets.tar.enc b/system_tests/secrets.tar.enc
new file mode 100644
index 0000000..5f20b1e
--- /dev/null
+++ b/system_tests/secrets.tar.enc
Binary files differ
diff --git a/system_tests/system_tests_async/__init__.py b/system_tests/system_tests_async/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/system_tests/system_tests_async/__init__.py
diff --git a/system_tests/system_tests_async/conftest.py b/system_tests/system_tests_async/conftest.py
new file mode 100644
index 0000000..9669099
--- /dev/null
+++ b/system_tests/system_tests_async/conftest.py
@@ -0,0 +1,115 @@
+# 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.
+
+import json
+import os
+
+from google.auth import _helpers
+import google.auth.transport.requests
+import google.auth.transport.urllib3
+import pytest
+import requests
+import urllib3
+
+import aiohttp
+from google.auth.transport import _aiohttp_requests as aiohttp_requests
+from system_tests.system_tests_sync import conftest as sync_conftest
+
+
+TOKEN_INFO_URL = "https://www.googleapis.com/oauth2/v3/tokeninfo"
+
+
[email protected]
+def service_account_file():
+    """The full path to a valid service account key file."""
+    yield sync_conftest.SERVICE_ACCOUNT_FILE
+
+
[email protected]
+def impersonated_service_account_file():
+    """The full path to a valid service account key file."""
+    yield sync_conftest.IMPERSONATED_SERVICE_ACCOUNT_FILE
+
+
[email protected]
+def authorized_user_file():
+    """The full path to a valid authorized user file."""
+    yield sync_conftest.AUTHORIZED_USER_FILE
+
+
[email protected]
+async def aiohttp_session():
+    async with aiohttp.ClientSession(auto_decompress=False) as session:
+        yield session
+
+
[email protected](params=["aiohttp"])
+async def http_request(request, aiohttp_session):
+    """A transport.request object."""
+    yield aiohttp_requests.Request(aiohttp_session)
+
+
[email protected]
+async def token_info(http_request):
+    """Returns a function that obtains OAuth2 token info."""
+
+    async def _token_info(access_token=None, id_token=None):
+        query_params = {}
+
+        if access_token is not None:
+            query_params["access_token"] = access_token
+        elif id_token is not None:
+            query_params["id_token"] = id_token
+        else:
+            raise ValueError("No token specified.")
+
+        url = _helpers.update_query(sync_conftest.TOKEN_INFO_URL, query_params)
+
+        response = await http_request(url=url, method="GET")
+
+        data = await response.content()
+
+        return json.loads(data.decode("utf-8"))
+
+    yield _token_info
+
+
[email protected]
+async def verify_refresh(http_request):
+    """Returns a function that verifies that credentials can be refreshed."""
+
+    async def _verify_refresh(credentials):
+        if credentials.requires_scopes:
+            credentials = credentials.with_scopes(["email", "profile"])
+
+        await credentials.refresh(http_request)
+
+        assert credentials.token
+        assert credentials.valid
+
+    yield _verify_refresh
+
+
+def verify_environment():
+    """Checks to make sure that requisite data files are available."""
+    if not os.path.isdir(sync_conftest.DATA_DIR):
+        raise EnvironmentError(
+            "In order to run system tests, test data must exist in "
+            "system_tests/data. See CONTRIBUTING.rst for details."
+        )
+
+
+def pytest_configure(config):
+    """Pytest hook that runs before Pytest collects any tests."""
+    verify_environment()
diff --git a/system_tests/system_tests_async/test_default.py b/system_tests/system_tests_async/test_default.py
new file mode 100644
index 0000000..32299c0
--- /dev/null
+++ b/system_tests/system_tests_async/test_default.py
@@ -0,0 +1,29 @@
+# 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.
+
+import os
+import pytest
+
+from google.auth import _default_async
+
+EXPECT_PROJECT_ID = os.environ.get("EXPECT_PROJECT_ID")
+
[email protected]
+async def test_application_default_credentials(verify_refresh):
+    credentials, project_id = _default_async.default_async()
+
+    if EXPECT_PROJECT_ID is not None:
+        assert project_id is not None
+
+    await verify_refresh(credentials)
diff --git a/system_tests/system_tests_async/test_id_token.py b/system_tests/system_tests_async/test_id_token.py
new file mode 100644
index 0000000..a21b137
--- /dev/null
+++ b/system_tests/system_tests_async/test_id_token.py
@@ -0,0 +1,25 @@
+# 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.
+import pytest
+
+from google.auth import jwt
+import google.oauth2._id_token_async
+
[email protected]
+async def test_fetch_id_token(http_request):
+    audience = "https://pubsub.googleapis.com"
+    token = await google.oauth2._id_token_async.fetch_id_token(http_request, audience)
+
+    _, payload, _, _ = jwt._unverified_decode(token)
+    assert payload["aud"] == audience
diff --git a/system_tests/system_tests_async/test_service_account.py b/system_tests/system_tests_async/test_service_account.py
new file mode 100644
index 0000000..c1c16cc
--- /dev/null
+++ b/system_tests/system_tests_async/test_service_account.py
@@ -0,0 +1,53 @@
+# 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.
+
+import pytest
+
+from google.auth import _helpers
+from google.auth import exceptions
+from google.auth import iam
+from google.oauth2 import _service_account_async
+
+
[email protected]
+def credentials(service_account_file):
+    yield _service_account_async.Credentials.from_service_account_file(service_account_file)
+
+
[email protected]
+async def test_refresh_no_scopes(http_request, credentials):
+    """
+    We expect the http request to refresh credentials
+    without scopes provided to throw an error.
+    """
+    with pytest.raises(exceptions.RefreshError):
+        await credentials.refresh(http_request)
+
[email protected]
+async def test_refresh_success(http_request, credentials, token_info):
+    credentials = credentials.with_scopes(["email", "profile"])
+    await credentials.refresh(http_request)
+
+    assert credentials.token
+
+    info = await token_info(credentials.token)
+
+    assert info["email"] == credentials.service_account_email
+    info_scopes = _helpers.string_to_scopes(info["scope"])
+    assert set(info_scopes) == set(
+        [
+            "https://www.googleapis.com/auth/userinfo.email",
+            "https://www.googleapis.com/auth/userinfo.profile",
+        ]
+    )
diff --git a/system_tests/system_tests_sync/.gitignore b/system_tests/system_tests_sync/.gitignore
new file mode 100644
index 0000000..be60550
--- /dev/null
+++ b/system_tests/system_tests_sync/.gitignore
@@ -0,0 +1,2 @@
+data
+secrets.tar
\ No newline at end of file
diff --git a/system_tests/system_tests_sync/__init__.py b/system_tests/system_tests_sync/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/system_tests/system_tests_sync/__init__.py
diff --git a/system_tests/system_tests_sync/app_engine_test_app/.gitignore b/system_tests/system_tests_sync/app_engine_test_app/.gitignore
new file mode 100644
index 0000000..7951405
--- /dev/null
+++ b/system_tests/system_tests_sync/app_engine_test_app/.gitignore
@@ -0,0 +1 @@
+lib
\ No newline at end of file
diff --git a/system_tests/system_tests_sync/app_engine_test_app/app.yaml b/system_tests/system_tests_sync/app_engine_test_app/app.yaml
new file mode 100644
index 0000000..06f2270
--- /dev/null
+++ b/system_tests/system_tests_sync/app_engine_test_app/app.yaml
@@ -0,0 +1,12 @@
+api_version: 1
+service: google-auth-system-tests
+runtime: python27
+threadsafe: true
+
+handlers:
+- url: .*
+  script: main.app
+
+libraries:
+- name: ssl
+  version: 2.7.11
\ No newline at end of file
diff --git a/system_tests/system_tests_sync/app_engine_test_app/appengine_config.py b/system_tests/system_tests_sync/app_engine_test_app/appengine_config.py
new file mode 100644
index 0000000..1197ab5
--- /dev/null
+++ b/system_tests/system_tests_sync/app_engine_test_app/appengine_config.py
@@ -0,0 +1,30 @@
+# Copyright 2016 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.
+
+from google.appengine.ext import vendor
+
+# Add any libraries installed in the "lib" folder.
+vendor.add("lib")
+
+
+# Patch os.path.expanduser. This should be fixed in GAE
+# versions released after Nov 2016.
+import os.path
+
+
+def patched_expanduser(path):
+    return path
+
+
+os.path.expanduser = patched_expanduser
\ No newline at end of file
diff --git a/system_tests/system_tests_sync/app_engine_test_app/main.py b/system_tests/system_tests_sync/app_engine_test_app/main.py
new file mode 100644
index 0000000..f44ed4c
--- /dev/null
+++ b/system_tests/system_tests_sync/app_engine_test_app/main.py
@@ -0,0 +1,129 @@
+# Copyright 2016 Google LLC All Rights Reserved.
+#
+# 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.
+
+"""App Engine standard application that runs basic system tests for
+google.auth.app_engine.
+This application has to run tests manually instead of using pytest because
+pytest currently doesn't work on App Engine standard.
+"""
+
+import contextlib
+import json
+import sys
+from StringIO import StringIO
+import traceback
+
+from google.appengine.api import app_identity
+import google.auth
+from google.auth import _helpers
+from google.auth import app_engine
+import google.auth.transport.urllib3
+import urllib3.contrib.appengine
+import webapp2
+
+FAILED_TEST_TMPL = """
+Test {} failed: {}
+Stacktrace:
+{}
+Captured output:
+{}
+"""
+TOKEN_INFO_URL = "https://www.googleapis.com/oauth2/v3/tokeninfo"
+EMAIL_SCOPE = "https://www.googleapis.com/auth/userinfo.email"
+HTTP = urllib3.contrib.appengine.AppEngineManager()
+HTTP_REQUEST = google.auth.transport.urllib3.Request(HTTP)
+
+
+def test_credentials():
+    credentials = app_engine.Credentials()
+    scoped_credentials = credentials.with_scopes([EMAIL_SCOPE])
+
+    scoped_credentials.refresh(None)
+
+    assert scoped_credentials.valid
+    assert scoped_credentials.token is not None
+
+    # Get token info and verify scope
+    url = _helpers.update_query(
+        TOKEN_INFO_URL, {"access_token": scoped_credentials.token}
+    )
+    response = HTTP_REQUEST(url=url, method="GET")
+    token_info = json.loads(response.data.decode("utf-8"))
+
+    assert token_info["scope"] == EMAIL_SCOPE
+
+
+def test_default():
+    credentials, project_id = google.auth.default()
+
+    assert isinstance(credentials, app_engine.Credentials)
+    assert project_id == app_identity.get_application_id()
+
+
[email protected]
+def capture():
+    """Context manager that captures stderr and stdout."""
+    oldout, olderr = sys.stdout, sys.stderr
+    try:
+        out = StringIO()
+        sys.stdout, sys.stderr = out, out
+        yield out
+    finally:
+        sys.stdout, sys.stderr = oldout, olderr
+
+
+def run_test_func(func):
+    with capture() as capsys:
+        try:
+            func()
+            return True, ""
+        except Exception as exc:
+            output = FAILED_TEST_TMPL.format(
+                func.func_name, exc, traceback.format_exc(), capsys.getvalue()
+            )
+            return False, output
+
+
+def run_tests():
+    """Runs all tests.
+    Returns:
+        Tuple[bool, str]: A tuple containing True if all tests pass, False
+        otherwise, and any captured output from the tests.
+    """
+    status = True
+    output = ""
+
+    tests = (test_credentials, test_default)
+
+    for test in tests:
+        test_status, test_output = run_test_func(test)
+        status = status and test_status
+        output += test_output
+
+    return status, output
+
+
+class MainHandler(webapp2.RequestHandler):
+    def get(self):
+        self.response.headers["content-type"] = "text/plain"
+
+        status, output = run_tests()
+
+        if not status:
+            self.response.status = 500
+
+        self.response.write(output)
+
+
+app = webapp2.WSGIApplication([("/", MainHandler)], debug=True)
\ No newline at end of file
diff --git a/system_tests/system_tests_sync/app_engine_test_app/requirements.txt b/system_tests/system_tests_sync/app_engine_test_app/requirements.txt
new file mode 100644
index 0000000..cb8a382
--- /dev/null
+++ b/system_tests/system_tests_sync/app_engine_test_app/requirements.txt
@@ -0,0 +1,3 @@
+urllib3
+# Relative path to google-auth-python's source.
+../../..
\ No newline at end of file
diff --git a/system_tests/system_tests_sync/conftest.py b/system_tests/system_tests_sync/conftest.py
new file mode 100644
index 0000000..16caa65
--- /dev/null
+++ b/system_tests/system_tests_sync/conftest.py
@@ -0,0 +1,141 @@
+# Copyright 2016 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 json
+import os
+
+from google.auth import _helpers
+import google.auth.transport.requests
+import google.auth.transport.urllib3
+import pytest
+import requests
+import urllib3
+
+
+HERE = os.path.dirname(__file__)
+DATA_DIR = os.path.join(HERE, "../data")
+IMPERSONATED_SERVICE_ACCOUNT_FILE = os.path.join(
+    DATA_DIR, "impersonated_service_account.json"
+)
+SERVICE_ACCOUNT_FILE = os.path.join(DATA_DIR, "service_account.json")
+URLLIB3_HTTP = urllib3.PoolManager(retries=False)
+REQUESTS_SESSION = requests.Session()
+REQUESTS_SESSION.verify = False
+TOKEN_INFO_URL = "https://www.googleapis.com/oauth2/v3/tokeninfo"
+
+
[email protected]
+def service_account_file():
+    """The full path to a valid service account key file."""
+    yield SERVICE_ACCOUNT_FILE
+
+
[email protected]
+def impersonated_service_account_file():
+    """The full path to a valid service account key file."""
+    yield IMPERSONATED_SERVICE_ACCOUNT_FILE
+
+
[email protected]
+def authorized_user_file():
+    """The full path to a valid authorized user file."""
+    yield AUTHORIZED_USER_FILE
+
+
[email protected](params=["urllib3", "requests"])
+def request_type(request):
+    yield request.param
+
+
[email protected]
+def http_request(request_type):
+    """A transport.request object."""
+    if request_type == "urllib3":
+        yield google.auth.transport.urllib3.Request(URLLIB3_HTTP)
+    elif request_type == "requests":
+        yield google.auth.transport.requests.Request(REQUESTS_SESSION)
+
+
[email protected]
+def authenticated_request(request_type):
+    """A transport.request object that takes credentials"""
+    if request_type == "urllib3":
+
+        def wrapper(credentials):
+            return google.auth.transport.urllib3.AuthorizedHttp(
+                credentials, http=URLLIB3_HTTP
+            ).request
+
+        yield wrapper
+    elif request_type == "requests":
+
+        def wrapper(credentials):
+            session = google.auth.transport.requests.AuthorizedSession(credentials)
+            session.verify = False
+            return google.auth.transport.requests.Request(session)
+
+        yield wrapper
+
+
[email protected]
+def token_info(http_request):
+    """Returns a function that obtains OAuth2 token info."""
+
+    def _token_info(access_token=None, id_token=None):
+        query_params = {}
+
+        if access_token is not None:
+            query_params["access_token"] = access_token
+        elif id_token is not None:
+            query_params["id_token"] = id_token
+        else:
+            raise ValueError("No token specified.")
+
+        url = _helpers.update_query(TOKEN_INFO_URL, query_params)
+
+        response = http_request(url=url, method="GET")
+
+        return json.loads(response.data.decode("utf-8"))
+
+    yield _token_info
+
+
[email protected]
+def verify_refresh(http_request):
+    """Returns a function that verifies that credentials can be refreshed."""
+
+    def _verify_refresh(credentials):
+        if credentials.requires_scopes:
+            credentials = credentials.with_scopes(["email", "profile"])
+
+        credentials.refresh(http_request)
+
+        assert credentials.token
+        assert credentials.valid
+
+    yield _verify_refresh
+
+
+def verify_environment():
+    """Checks to make sure that requisite data files are available."""
+    if not os.path.isdir(DATA_DIR):
+        raise EnvironmentError(
+            "In order to run system tests, test data must exist in "
+            "system_tests/data. See CONTRIBUTING.rst for details."
+        )
+
+
+def pytest_configure(config):
+    """Pytest hook that runs before Pytest collects any tests."""
+    verify_environment()
diff --git a/system_tests/system_tests_sync/secrets.tar.enc b/system_tests/system_tests_sync/secrets.tar.enc
new file mode 100644
index 0000000..29e0692
--- /dev/null
+++ b/system_tests/system_tests_sync/secrets.tar.enc
Binary files differ
diff --git a/system_tests/system_tests_sync/test_app_engine.py b/system_tests/system_tests_sync/test_app_engine.py
new file mode 100644
index 0000000..79776ce
--- /dev/null
+++ b/system_tests/system_tests_sync/test_app_engine.py
@@ -0,0 +1,22 @@
+# Copyright 2016 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 os
+
+TEST_APP_URL = os.environ["TEST_APP_URL"]
+
+
+def test_live_application(http_request):
+    response = http_request(method="GET", url=TEST_APP_URL)
+    assert response.status == 200, response.data.decode("utf-8")
\ No newline at end of file
diff --git a/system_tests/system_tests_sync/test_compute_engine.py b/system_tests/system_tests_sync/test_compute_engine.py
new file mode 100644
index 0000000..1e0eaf1
--- /dev/null
+++ b/system_tests/system_tests_sync/test_compute_engine.py
@@ -0,0 +1,75 @@
+# Copyright 2016 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.
+
+from datetime import datetime
+
+import pytest
+
+import google.auth
+from google.auth import compute_engine
+from google.auth import _helpers
+from google.auth import exceptions
+from google.auth import jwt
+from google.auth.compute_engine import _metadata
+import google.oauth2.id_token
+
+AUDIENCE = "https://pubsub.googleapis.com"
+
+
[email protected](autouse=True)
+def check_gce_environment(http_request):
+    try:
+        _metadata.get_service_account_info(http_request)
+    except exceptions.TransportError:
+        pytest.skip("Compute Engine metadata service is not available.")
+
+
+def test_refresh(http_request, token_info):
+    credentials = compute_engine.Credentials()
+
+    credentials.refresh(http_request)
+
+    assert credentials.token is not None
+    assert credentials.service_account_email is not None
+
+    info = token_info(credentials.token)
+    info_scopes = _helpers.string_to_scopes(info["scope"])
+    assert set(info_scopes) == set(credentials.scopes)
+
+
+def test_default(verify_refresh):
+    credentials, project_id = google.auth.default()
+
+    assert project_id is not None
+    assert isinstance(credentials, compute_engine.Credentials)
+    verify_refresh(credentials)
+
+
+def test_id_token_from_metadata(http_request):
+    credentials = compute_engine.IDTokenCredentials(
+        http_request, AUDIENCE, use_metadata_identity_endpoint=True
+    )
+    credentials.refresh(http_request)
+
+    _, payload, _, _ = jwt._unverified_decode(credentials.token)
+    assert credentials.valid
+    assert payload["aud"] == AUDIENCE
+    assert datetime.fromtimestamp(payload["exp"]) == credentials.expiry
+
+
+def test_fetch_id_token(http_request):
+    token = google.oauth2.id_token.fetch_id_token(http_request, AUDIENCE)
+
+    _, payload, _, _ = jwt._unverified_decode(token)
+    assert payload["aud"] == AUDIENCE
diff --git a/system_tests/system_tests_sync/test_default.py b/system_tests/system_tests_sync/test_default.py
new file mode 100644
index 0000000..560ab32
--- /dev/null
+++ b/system_tests/system_tests_sync/test_default.py
@@ -0,0 +1,28 @@
+# Copyright 2016 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 os
+
+import google.auth
+
+EXPECT_PROJECT_ID = os.environ.get("EXPECT_PROJECT_ID")
+
+
+def test_application_default_credentials(verify_refresh):
+    credentials, project_id = google.auth.default()
+
+    if EXPECT_PROJECT_ID is not None:
+        assert project_id is not None
+
+    verify_refresh(credentials)
diff --git a/system_tests/system_tests_sync/test_downscoping.py b/system_tests/system_tests_sync/test_downscoping.py
new file mode 100644
index 0000000..fdb4efa
--- /dev/null
+++ b/system_tests/system_tests_sync/test_downscoping.py
@@ -0,0 +1,162 @@
+# 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 re
+import uuid
+
+import google.auth
+
+from google.auth import downscoped
+from google.auth.transport import requests
+from google.cloud import exceptions
+from google.cloud import storage
+from google.oauth2 import credentials
+
+import pytest
+
+ # The object prefix used to test access to files beginning with this prefix.
+_OBJECT_PREFIX = "customer-a"
+# The object name of the object inaccessible by the downscoped token.
+_ACCESSIBLE_OBJECT_NAME = "{0}-data.txt".format(_OBJECT_PREFIX)
+# The content of the object accessible by the downscoped token.
+_ACCESSIBLE_CONTENT = "hello world"
+# The content of the object inaccessible by the downscoped token.
+_INACCESSIBLE_CONTENT = "secret content"
+# The object name of the object inaccessible by the downscoped token.
+_INACCESSIBLE_OBJECT_NAME = "other-customer-data.txt"
+
+
[email protected](scope="module")
+def temp_bucket():
+    """Yields a bucket that is deleted after the test completes."""
+    bucket = None
+    while bucket is None or bucket.exists():
+        bucket_name = "auth-python-downscope-test-{}".format(uuid.uuid4())
+        bucket = storage.Client().bucket(bucket_name)
+    bucket = storage.Client().create_bucket(bucket.name)
+    yield bucket
+    bucket.delete(force=True)
+
+
[email protected](scope="module")
+def temp_blobs(temp_bucket):
+    """Yields two blobs that are deleted after the test completes."""
+    bucket = temp_bucket
+    # Downscoped tokens will have readonly access to this blob.
+    accessible_blob = bucket.blob(_ACCESSIBLE_OBJECT_NAME)
+    accessible_blob.upload_from_string(_ACCESSIBLE_CONTENT)
+    # Downscoped tokens will have no access to this blob.
+    inaccessible_blob = bucket.blob(_INACCESSIBLE_OBJECT_NAME)
+    inaccessible_blob.upload_from_string(_INACCESSIBLE_CONTENT)
+    yield (accessible_blob, inaccessible_blob)
+    bucket.delete_blobs([accessible_blob, inaccessible_blob])
+
+
+def get_token_from_broker(bucket_name, object_prefix):
+    """Simulates token broker generating downscoped tokens for specified bucket.
+
+    Args:
+        bucket_name (str): The name of the Cloud Storage bucket.
+        object_prefix (str): The prefix string of the object name. This is used
+            to ensure access is restricted to only objects starting with this
+            prefix string.
+
+    Returns:
+        Tuple[str, datetime.datetime]: The downscoped access token and its expiry date.
+    """
+    # Initialize the Credential Access Boundary rules.
+    available_resource = "//storage.googleapis.com/projects/_/buckets/{0}".format(bucket_name)
+    # Downscoped credentials will have readonly access to the resource.
+    available_permissions = ["inRole:roles/storage.objectViewer"]
+    # Only objects starting with the specified prefix string in the object name
+    # will be allowed read access.
+    availability_expression = (
+        "resource.name.startsWith('projects/_/buckets/{0}/objects/{1}')".format(bucket_name, object_prefix)
+    )
+    availability_condition = downscoped.AvailabilityCondition(availability_expression)
+    # Define the single access boundary rule using the above properties.
+    rule = downscoped.AccessBoundaryRule(
+        available_resource=available_resource,
+        available_permissions=available_permissions,
+        availability_condition=availability_condition,
+    )
+    # Define the Credential Access Boundary with all the relevant rules.
+    credential_access_boundary = downscoped.CredentialAccessBoundary(rules=[rule])
+
+    # Retrieve the source credentials via ADC.
+    source_credentials, _ = google.auth.default()
+    if source_credentials.requires_scopes:
+        source_credentials = source_credentials.with_scopes(
+            ["https://www.googleapis.com/auth/cloud-platform"]
+        )
+
+    # Create the downscoped credentials.
+    downscoped_credentials = downscoped.Credentials(
+        source_credentials=source_credentials,
+        credential_access_boundary=credential_access_boundary,
+    )
+
+    # Refresh the tokens.
+    downscoped_credentials.refresh(requests.Request())
+
+    # These values will need to be passed to the token consumer.
+    access_token = downscoped_credentials.token
+    expiry = downscoped_credentials.expiry
+    return (access_token, expiry)
+
+
+def test_downscoping(temp_blobs):
+    """Tests token consumer access to cloud storage using downscoped tokens.
+
+    Args:
+        temp_blobs (Tuple[google.cloud.storage.blob.Blob, ...]): The temporarily
+            created test cloud storage blobs (one readonly accessible, the other
+            not).
+    """
+    accessible_blob, inaccessible_blob = temp_blobs
+    bucket_name = accessible_blob.bucket.name
+    # Create the OAuth credentials from the downscoped token and pass a
+    # refresh handler to handle token expiration. We are passing a
+    # refresh_handler instead of a one-time access token/expiry pair.
+    # This will allow testing this on-demand method for getting access tokens.
+    def refresh_handler(request, scopes=None):
+        # Get readonly access tokens to objects with accessible prefix in
+        # the temporarily created bucket.
+        return get_token_from_broker(bucket_name, _OBJECT_PREFIX)
+
+    creds = credentials.Credentials(
+        None,
+        scopes=["https://www.googleapis.com/auth/cloud-platform"],
+        refresh_handler=refresh_handler,
+    )
+
+    # Initialize a Cloud Storage client with the oauth2 credentials.
+    storage_client = storage.Client(credentials=creds)
+
+    # Test read access succeeds to accessible blob.
+    bucket = storage_client.bucket(bucket_name)
+    blob = bucket.blob(accessible_blob.name)
+    assert blob.download_as_bytes().decode("utf-8") == _ACCESSIBLE_CONTENT
+
+    # Test write access fails.
+    with pytest.raises(exceptions.Forbidden) as excinfo:
+        blob.upload_from_string("Write operations are not allowed")
+
+    assert excinfo.match(r"does not have storage.objects.create access")
+
+    # Test read access fails to inaccessible blob.
+    with pytest.raises(exceptions.Forbidden) as excinfo:
+        bucket.blob(inaccessible_blob.name).download_as_bytes()
+
+    assert excinfo.match(r"does not have storage.objects.get access")
diff --git a/system_tests/system_tests_sync/test_external_accounts.py b/system_tests/system_tests_sync/test_external_accounts.py
new file mode 100644
index 0000000..e24c7b4
--- /dev/null
+++ b/system_tests/system_tests_sync/test_external_accounts.py
@@ -0,0 +1,305 @@
+# 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.
+
+# Prerequisites:
+# Make sure to run the setup in scripts/setup_external_accounts.sh
+# and copy the logged constant strings (_AUDIENCE_OIDC, _AUDIENCE_AWS)
+# into this file before running this test suite.
+# Once that is done, this test can be run indefinitely.
+#
+# The only requirement for this test suite to run is to set the environment
+# variable GOOGLE_APPLICATION_CREDENTIALS to point to the expected service
+# account keys whose email is referred to in the setup script.
+#
+# This script follows the following logic.
+# OIDC provider (file-sourced and url-sourced credentials):
+# Use the service account keys to generate a Google ID token using the
+# iamcredentials generateIdToken API, using the default STS audience.
+# This will use the service account client ID as the sub field of the token.
+# This OIDC token will be used as the external subject token to be exchanged
+# for a Google access token via GCP STS endpoint and then to impersonate the
+# original service account key.
+
+
+import json
+import os
+import socket
+from tempfile import NamedTemporaryFile
+import threading
+
+import sys
+import google.auth
+from googleapiclient import discovery
+from six.moves import BaseHTTPServer
+from google.oauth2 import service_account
+import pytest
+from mock import patch
+
+# Populate values from the output of scripts/setup_external_accounts.sh.
+_AUDIENCE_OIDC = "//iam.googleapis.com/projects/79992041559/locations/global/workloadIdentityPools/pool-73wslmxn/providers/oidc-73wslmxn"
+_AUDIENCE_AWS = "//iam.googleapis.com/projects/79992041559/locations/global/workloadIdentityPools/pool-73wslmxn/providers/aws-73wslmxn"
+_ROLE_AWS = "arn:aws:iam::077071391996:role/ci-python-test"
+
+
+def dns_access_direct(request, project_id):
+    # First, get the default credentials.
+    credentials, _ = google.auth.default(
+        scopes=["https://www.googleapis.com/auth/cloud-platform.read-only"],
+        request=request,
+    )
+
+    # Apply the default credentials to the headers to make the request.
+    headers = {}
+    credentials.apply(headers)
+    response = request(
+        url="https://dns.googleapis.com/dns/v1/projects/{}".format(project_id),
+        headers=headers,
+    )
+
+    if response.status == 200:
+        return response.data
+
+
+def dns_access_client_library(_, project_id):
+    service = discovery.build("dns", "v1")
+    request = service.projects().get(project=project_id)
+    return request.execute()
+
+
[email protected](params=[dns_access_direct, dns_access_client_library])
+def dns_access(request, http_request, service_account_info):
+    # Fill in the fixtures on the functions,
+    # so that we don't have to fill in the parameters manually.
+    def wrapper():
+        return request.param(http_request, service_account_info["project_id"])
+
+    yield wrapper
+
+
[email protected]
+def oidc_credentials(service_account_file, http_request):
+    result = service_account.IDTokenCredentials.from_service_account_file(
+        service_account_file, target_audience=_AUDIENCE_OIDC
+    )
+    result.refresh(http_request)
+    yield result
+
+
[email protected]
+def service_account_info(service_account_file):
+    with open(service_account_file) as f:
+        yield json.load(f)
+
+
[email protected]
+def aws_oidc_credentials(
+    service_account_file, service_account_info, authenticated_request
+):
+    credentials = service_account.Credentials.from_service_account_file(
+        service_account_file, scopes=["https://www.googleapis.com/auth/cloud-platform"]
+    )
+    result = authenticated_request(credentials)(
+        url="https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/{}:generateIdToken".format(
+            service_account_info["client_email"]
+        ),
+        method="POST",
+        body=json.dumps(
+            {"audience": service_account_info["client_id"], "includeEmail": True}
+        ),
+    )
+    assert result.status == 200
+
+    yield json.loads(result.data)["token"]
+
+
+# Our external accounts tests involve setting up some preconditions, setting a
+# credential file, and then making sure that our client libraries can work with
+# the set credentials.
+def get_project_dns(dns_access, credential_data):
+    with NamedTemporaryFile() as credfile:
+        credfile.write(json.dumps(credential_data).encode("utf-8"))
+        credfile.flush()
+        old_credentials = os.environ.get("GOOGLE_APPLICATION_CREDENTIALS")
+
+        with patch.dict(os.environ, {"GOOGLE_APPLICATION_CREDENTIALS": credfile.name}):
+            # If our setup and credential file are correct,
+            # discovery.build should be able to establish these as the default credentials.
+            return dns_access()
+
+
+def get_xml_value_by_tagname(data, tagname):
+    startIndex = data.index("<{}>".format(tagname))
+    if startIndex >= 0:
+        endIndex = data.index("</{}>".format(tagname), startIndex)
+        if endIndex > startIndex:
+            return data[startIndex + len(tagname) + 2 : endIndex]
+
+
+# This test makes sure that setting an accesible credential file
+# works to allow access to Google resources.
+def test_file_based_external_account(
+    oidc_credentials, service_account_info, dns_access
+):
+    with NamedTemporaryFile() as tmpfile:
+        tmpfile.write(oidc_credentials.token.encode("utf-8"))
+        tmpfile.flush()
+
+        assert get_project_dns(
+            dns_access,
+            {
+                "type": "external_account",
+                "audience": _AUDIENCE_OIDC,
+                "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
+                "token_url": "https://sts.googleapis.com/v1/token",
+                "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/{}:generateAccessToken".format(
+                    oidc_credentials.service_account_email
+                ),
+                "credential_source": {
+                    "file": tmpfile.name,
+                },
+            },
+        )
+
+
+# This test makes sure that setting up an http server to provide credentials
+# works to allow access to Google resources.
+def test_url_based_external_account(dns_access, oidc_credentials, service_account_info):
+    class TestResponseHandler(BaseHTTPServer.BaseHTTPRequestHandler):
+        def do_GET(self):
+            if self.headers["my-header"] != "expected-value":
+                self.send_response(400)
+                self.send_header("Content-Type", "application/json")
+                self.end_headers()
+                self.wfile.write(
+                    json.dumps({"error": "missing header"}).encode("utf-8")
+                )
+            elif self.path != "/token":
+                self.send_response(400)
+                self.send_header("Content-Type", "application/json")
+                self.end_headers()
+                self.wfile.write(
+                    json.dumps({"error": "incorrect token path"}).encode("utf-8")
+                )
+            else:
+                self.send_response(200)
+                self.send_header("Content-Type", "application/json")
+                self.end_headers()
+                self.wfile.write(
+                    json.dumps({"access_token": oidc_credentials.token}).encode("utf-8")
+                )
+
+    class TestHTTPServer(BaseHTTPServer.HTTPServer, object):
+        def __init__(self):
+            self.port = self._find_open_port()
+            super(TestHTTPServer, self).__init__(("", self.port), TestResponseHandler)
+
+        @staticmethod
+        def _find_open_port():
+            s = socket.socket()
+            s.bind(("", 0))
+            return s.getsockname()[1]
+
+        # This makes sure that the server gets shut down when this variable leaves its "with" block
+        # The python3 HttpServer has __enter__ and __exit__ methods, but python2 does not.
+        # By redefining the __enter__ and __exit__ methods, we ensure that python2 and python3 act similarly
+        def __exit__(self, *args):
+            self.shutdown()
+
+        def __enter__(self):
+            return self
+
+    with TestHTTPServer() as server:
+        threading.Thread(target=server.serve_forever).start()
+
+        assert get_project_dns(
+            dns_access,
+            {
+                "type": "external_account",
+                "audience": _AUDIENCE_OIDC,
+                "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
+                "token_url": "https://sts.googleapis.com/v1/token",
+                "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/{}:generateAccessToken".format(
+                    oidc_credentials.service_account_email
+                ),
+                "credential_source": {
+                    "url": "http://localhost:{}/token".format(server.port),
+                    "headers": {"my-header": "expected-value"},
+                    "format": {
+                        "type": "json",
+                        "subject_token_field_name": "access_token",
+                    },
+                },
+            },
+        )
+
+
+# AWS provider tests for AWS credentials
+# The test suite will also run tests for AWS credentials. This works as
+# follows. (Note prequisite setup is needed. This is documented in
+# setup_external_accounts.sh).
+# - iamcredentials:generateIdToken is used to generate a Google ID token using
+#   the service account access token. The service account client_id is used as
+#   audience.
+# - AWS STS AssumeRoleWithWebIdentity API is used to exchange this token for
+#   temporary AWS security credentials for a specified AWS ARN role.
+# - AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_SESSION_TOKEN
+#   environment variables are set using these credentials before the test is
+#   run simulating an AWS VM.
+# - The test can now be run.
+def test_aws_based_external_account(
+    aws_oidc_credentials, service_account_info, dns_access, http_request
+):
+
+    response = http_request(
+        url=(
+            "https://sts.amazonaws.com/"
+            "?Action=AssumeRoleWithWebIdentity"
+            "&Version=2011-06-15"
+            "&DurationSeconds=3600"
+            "&RoleSessionName=python-test"
+            "&RoleArn={}"
+            "&WebIdentityToken={}"
+        ).format(_ROLE_AWS, aws_oidc_credentials)
+    )
+    assert response.status == 200
+
+    # The returned data is in XML, but loading an XML parser would be overkill.
+    # Searching the return text manually for the start and finish tag.
+    data = response.data.decode("utf-8")
+
+    with patch.dict(
+        os.environ,
+        {
+            "AWS_REGION": "us-east-2",
+            "AWS_ACCESS_KEY_ID": get_xml_value_by_tagname(data, "AccessKeyId"),
+            "AWS_SECRET_ACCESS_KEY": get_xml_value_by_tagname(data, "SecretAccessKey"),
+            "AWS_SESSION_TOKEN": get_xml_value_by_tagname(data, "SessionToken"),
+        },
+    ):
+        assert get_project_dns(
+            dns_access,
+            {
+                "type": "external_account",
+                "audience": _AUDIENCE_AWS,
+                "subject_token_type": "urn:ietf:params:aws:token-type:aws4_request",
+                "token_url": "https://sts.googleapis.com/v1/token",
+                "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/{}:generateAccessToken".format(
+                    service_account_info["client_email"]
+                ),
+                "credential_source": {
+                    "environment_id": "aws1",
+                    "regional_cred_verification_url": "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15",
+                },
+            },
+        )
diff --git a/system_tests/system_tests_sync/test_grpc.py b/system_tests/system_tests_sync/test_grpc.py
new file mode 100644
index 0000000..7f548ec
--- /dev/null
+++ b/system_tests/system_tests_sync/test_grpc.py
@@ -0,0 +1,93 @@
+# Copyright 2016 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 google.auth
+import google.auth.credentials
+import google.auth.jwt
+import google.auth.transport.grpc
+from google.oauth2 import service_account
+
+from google.cloud import pubsub_v1
+
+
+def test_grpc_request_with_regular_credentials(http_request):
+    credentials, project_id = google.auth.default()
+    credentials = google.auth.credentials.with_scopes_if_required(
+        credentials, scopes=["https://www.googleapis.com/auth/pubsub"]
+    )
+
+
+    # Create a pub/sub client.
+    client = pubsub_v1.PublisherClient(credentials=credentials)
+
+    # list the topics and drain the iterator to test that an authorized API
+    # call works.
+    list_topics_iter = client.list_topics(project="projects/{}".format(project_id))
+    list(list_topics_iter)
+
+
+def test_grpc_request_with_regular_credentials_and_self_signed_jwt(http_request):
+    credentials, project_id = google.auth.default()
+
+    # At the time this test is being written, there are no GAPIC libraries
+    # that will trigger the self-signed JWT flow. Manually create the self-signed
+    # jwt on the service account credential to check that the request
+    # succeeds.
+    credentials = credentials.with_scopes(
+        scopes=[], default_scopes=["https://www.googleapis.com/auth/pubsub"]
+    )
+    credentials._create_self_signed_jwt(audience="https://pubsub.googleapis.com/")
+
+    # Create a pub/sub client.
+    client = pubsub_v1.PublisherClient(credentials=credentials)
+
+    # list the topics and drain the iterator to test that an authorized API
+    # call works.
+    list_topics_iter = client.list_topics(project="projects/{}".format(project_id))
+    list(list_topics_iter)
+    
+    # Check that self-signed JWT was created and is being used
+    assert credentials._jwt_credentials is not None
+    assert credentials._jwt_credentials.token == credentials.token
+
+
+def test_grpc_request_with_jwt_credentials():
+    credentials, project_id = google.auth.default()
+    audience = "https://pubsub.googleapis.com/google.pubsub.v1.Publisher"
+    credentials = google.auth.jwt.Credentials.from_signing_credentials(
+        credentials, audience=audience
+    )
+
+    # Create a pub/sub client.
+    client = pubsub_v1.PublisherClient(credentials=credentials)
+
+    # list the topics and drain the iterator to test that an authorized API
+    # call works.
+    list_topics_iter = client.list_topics(project="projects/{}".format(project_id))
+    list(list_topics_iter)
+
+
+def test_grpc_request_with_on_demand_jwt_credentials():
+    credentials, project_id = google.auth.default()
+    credentials = google.auth.jwt.OnDemandCredentials.from_signing_credentials(
+        credentials
+    )
+
+    # Create a pub/sub client.
+    client = pubsub_v1.PublisherClient(credentials=credentials)
+
+    # list the topics and drain the iterator to test that an authorized API
+    # call works.
+    list_topics_iter = client.list_topics(project="projects/{}".format(project_id))
+    list(list_topics_iter)
diff --git a/system_tests/system_tests_sync/test_id_token.py b/system_tests/system_tests_sync/test_id_token.py
new file mode 100644
index 0000000..b07cefc
--- /dev/null
+++ b/system_tests/system_tests_sync/test_id_token.py
@@ -0,0 +1,25 @@
+# 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.
+import pytest
+
+from google.auth import jwt
+import google.oauth2.id_token
+
+
+def test_fetch_id_token(http_request):
+    audience = "https://pubsub.googleapis.com"
+    token = google.oauth2.id_token.fetch_id_token(http_request, audience)
+
+    _, payload, _, _ = jwt._unverified_decode(token)
+    assert payload["aud"] == audience
diff --git a/system_tests/system_tests_sync/test_impersonated_credentials.py b/system_tests/system_tests_sync/test_impersonated_credentials.py
new file mode 100644
index 0000000..6689e89
--- /dev/null
+++ b/system_tests/system_tests_sync/test_impersonated_credentials.py
@@ -0,0 +1,99 @@
+# 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.
+
+import json
+import pytest
+
+import google.oauth2.credentials
+from google.oauth2 import service_account
+import google.auth.impersonated_credentials
+from google.auth import _helpers
+
+
+GOOGLE_OAUTH2_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token"
+
+
[email protected]
+def service_account_credentials(service_account_file):
+    yield service_account.Credentials.from_service_account_file(service_account_file)
+
+
[email protected]
+def impersonated_service_account_credentials(impersonated_service_account_file):
+    yield service_account.Credentials.from_service_account_file(
+        impersonated_service_account_file
+    )
+
+
+def test_refresh_with_user_credentials_as_source(
+    authorized_user_file,
+    impersonated_service_account_credentials,
+    http_request,
+    token_info,
+):
+    with open(authorized_user_file, "r") as fh:
+        info = json.load(fh)
+
+    source_credentials = google.oauth2.credentials.Credentials(
+        None,
+        refresh_token=info["refresh_token"],
+        token_uri=GOOGLE_OAUTH2_TOKEN_ENDPOINT,
+        client_id=info["client_id"],
+        client_secret=info["client_secret"],
+        # The source credential needs this scope for the generateAccessToken request
+        # The user must also have `Service Account Token Creator` on the project
+        # that owns the impersonated service account.
+        # See https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials
+        scopes=["https://www.googleapis.com/auth/cloud-platform"],
+    )
+
+    source_credentials.refresh(http_request)
+
+    target_scopes = [
+        "https://www.googleapis.com/auth/devstorage.read_only",
+        "https://www.googleapis.com/auth/analytics",
+    ]
+    target_credentials = google.auth.impersonated_credentials.Credentials(
+        source_credentials=source_credentials,
+        target_principal=impersonated_service_account_credentials.service_account_email,
+        target_scopes=target_scopes,
+        lifetime=100,
+    )
+
+    target_credentials.refresh(http_request)
+    assert target_credentials.token
+
+
+def test_refresh_with_service_account_credentials_as_source(
+    http_request,
+    service_account_credentials,
+    impersonated_service_account_credentials,
+    token_info,
+):
+    source_credentials = service_account_credentials.with_scopes(["email"])
+    source_credentials.refresh(http_request)
+    assert source_credentials.token
+
+    target_scopes = [
+        "https://www.googleapis.com/auth/devstorage.read_only",
+        "https://www.googleapis.com/auth/analytics",
+    ]
+    target_credentials = google.auth.impersonated_credentials.Credentials(
+        source_credentials=source_credentials,
+        target_principal=impersonated_service_account_credentials.service_account_email,
+        target_scopes=target_scopes,
+    )
+
+    target_credentials.refresh(http_request)
+    assert target_credentials.token
diff --git a/system_tests/system_tests_sync/test_mtls_http.py b/system_tests/system_tests_sync/test_mtls_http.py
new file mode 100644
index 0000000..bcf2a59
--- /dev/null
+++ b/system_tests/system_tests_sync/test_mtls_http.py
@@ -0,0 +1,124 @@
+# 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.
+
+import json
+import mock
+import os
+import time
+from os import path
+
+
+import google.auth
+import google.auth.credentials
+from google.auth import environment_vars
+from google.auth.transport import mtls
+import google.auth.transport.requests
+import google.auth.transport.urllib3
+
+MTLS_ENDPOINT = "https://pubsub.mtls.googleapis.com/v1/projects/{}/topics"
+REGULAR_ENDPOINT = "https://pubsub.googleapis.com/v1/projects/{}/topics"
+
+
+def test_requests():
+    credentials, project_id = google.auth.default()
+    credentials = google.auth.credentials.with_scopes_if_required(
+        credentials, ["https://www.googleapis.com/auth/pubsub"]
+    )
+
+    authed_session = google.auth.transport.requests.AuthorizedSession(credentials)
+    with mock.patch.dict(os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}):
+        authed_session.configure_mtls_channel()
+
+    # If the devices has default client cert source, then a mutual TLS channel
+    # is supposed to be created.
+    assert authed_session.is_mtls == mtls.has_default_client_cert_source()
+
+    # Sleep 1 second to avoid 503 error.
+    time.sleep(1)
+
+    if authed_session.is_mtls:
+        response = authed_session.get(MTLS_ENDPOINT.format(project_id))
+    else:
+        response = authed_session.get(REGULAR_ENDPOINT.format(project_id))
+
+    assert response.ok
+
+
+def test_urllib3():
+    credentials, project_id = google.auth.default()
+    credentials = google.auth.credentials.with_scopes_if_required(
+        credentials, ["https://www.googleapis.com/auth/pubsub"]
+    )
+
+    authed_http = google.auth.transport.urllib3.AuthorizedHttp(credentials)
+    with mock.patch.dict(os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}):
+        is_mtls = authed_http.configure_mtls_channel()
+
+    # If the devices has default client cert source, then a mutual TLS channel
+    # is supposed to be created.
+    assert is_mtls == mtls.has_default_client_cert_source()
+
+    # Sleep 1 second to avoid 503 error.
+    time.sleep(1)
+
+    if is_mtls:
+        response = authed_http.request("GET", MTLS_ENDPOINT.format(project_id))
+    else:
+        response = authed_http.request("GET", REGULAR_ENDPOINT.format(project_id))
+
+    assert response.status == 200
+
+
+def test_requests_with_default_client_cert_source():
+    credentials, project_id = google.auth.default()
+    credentials = google.auth.credentials.with_scopes_if_required(
+        credentials, ["https://www.googleapis.com/auth/pubsub"]
+    )
+
+    authed_session = google.auth.transport.requests.AuthorizedSession(credentials)
+
+    if mtls.has_default_client_cert_source():
+        with mock.patch.dict(os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}):
+            authed_session.configure_mtls_channel(
+                client_cert_callback=mtls.default_client_cert_source()
+            )
+
+        assert authed_session.is_mtls
+
+        # Sleep 1 second to avoid 503 error.
+        time.sleep(1)
+
+        response = authed_session.get(MTLS_ENDPOINT.format(project_id))
+        assert response.ok
+
+
+def test_urllib3_with_default_client_cert_source():
+    credentials, project_id = google.auth.default()
+    credentials = google.auth.credentials.with_scopes_if_required(
+        credentials, ["https://www.googleapis.com/auth/pubsub"]
+    )
+
+    authed_http = google.auth.transport.urllib3.AuthorizedHttp(credentials)
+
+    if mtls.has_default_client_cert_source():
+        with mock.patch.dict(os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}):
+            assert authed_http.configure_mtls_channel(
+                client_cert_callback=mtls.default_client_cert_source()
+            )
+
+        # Sleep 1 second to avoid 503 error.
+        time.sleep(1)
+
+        response = authed_http.request("GET", MTLS_ENDPOINT.format(project_id))
+        assert response.status == 200
diff --git a/system_tests/system_tests_sync/test_oauth2_credentials.py b/system_tests/system_tests_sync/test_oauth2_credentials.py
new file mode 100644
index 0000000..908db31
--- /dev/null
+++ b/system_tests/system_tests_sync/test_oauth2_credentials.py
@@ -0,0 +1,55 @@
+# Copyright 2016 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 json
+
+from google.auth import _helpers
+import google.oauth2.credentials
+
+GOOGLE_OAUTH2_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token"
+
+
+def test_refresh(authorized_user_file, http_request, token_info):
+    with open(authorized_user_file, "r") as fh:
+        info = json.load(fh)
+
+    credentials = google.oauth2.credentials.Credentials(
+        None,  # No access token, must be refreshed.
+        refresh_token=info["refresh_token"],
+        token_uri=GOOGLE_OAUTH2_TOKEN_ENDPOINT,
+        client_id=info["client_id"],
+        client_secret=info["client_secret"],
+    )
+
+    credentials.refresh(http_request)
+
+    assert credentials.token
+
+    info = token_info(credentials.token)
+
+    info_scopes = _helpers.string_to_scopes(info["scope"])
+
+    # Canonical list of scopes at https://cloud.google.com/sdk/gcloud/reference/auth/application-default/login
+    # or do `gcloud auth application-defaut login --help`
+    canonical_scopes = set(
+        [
+            "https://www.googleapis.com/auth/userinfo.email",
+            "https://www.googleapis.com/auth/cloud-platform",
+            "openid",
+        ]
+    )
+    # When running the test locally, we always have an additional "accounts.reauth" scope.
+    canonical_scopes_with_reauth = canonical_scopes.copy()
+    canonical_scopes_with_reauth.add("https://www.googleapis.com/auth/accounts.reauth")
+    assert set(info_scopes) == canonical_scopes or set(info_scopes) == canonical_scopes_with_reauth
diff --git a/system_tests/system_tests_sync/test_requests.py b/system_tests/system_tests_sync/test_requests.py
new file mode 100644
index 0000000..2800484
--- /dev/null
+++ b/system_tests/system_tests_sync/test_requests.py
@@ -0,0 +1,42 @@
+# 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 google.auth
+import google.auth.credentials
+import google.auth.transport.requests
+from google.oauth2 import service_account
+
+
+def test_authorized_session_with_service_account_and_self_signed_jwt():
+    credentials, project_id = google.auth.default()
+
+    credentials = credentials.with_scopes(
+        scopes=[],
+        default_scopes=["https://www.googleapis.com/auth/pubsub"],
+    )
+
+    session = google.auth.transport.requests.AuthorizedSession(
+        credentials=credentials, default_host="pubsub.googleapis.com"
+    )
+
+    # List Pub/Sub Topics through the REST API
+    # https://cloud.google.com/pubsub/docs/reference/rest/v1/projects.topics/list
+    url = "https://pubsub.googleapis.com/v1/projects/{}/topics".format(project_id)
+    with session:
+        response = session.get(url)
+        response.raise_for_status()
+
+    # Check that self-signed JWT was created and is being used
+    assert credentials._jwt_credentials is not None
+    assert credentials._jwt_credentials.token == credentials.token
diff --git a/system_tests/system_tests_sync/test_service_account.py b/system_tests/system_tests_sync/test_service_account.py
new file mode 100644
index 0000000..498b75b
--- /dev/null
+++ b/system_tests/system_tests_sync/test_service_account.py
@@ -0,0 +1,65 @@
+# Copyright 2016 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 pytest
+
+from google.auth import _helpers
+from google.auth import exceptions
+from google.auth import iam
+from google.oauth2 import service_account
+
+
[email protected]
+def credentials(service_account_file):
+    yield service_account.Credentials.from_service_account_file(service_account_file)
+
+
+def test_refresh_no_scopes(http_request, credentials):
+    with pytest.raises(exceptions.RefreshError):
+        credentials.refresh(http_request)
+
+
+def test_refresh_success(http_request, credentials, token_info):
+    credentials = credentials.with_scopes(["email", "profile"])
+
+    credentials.refresh(http_request)
+
+    assert credentials.token
+
+    info = token_info(credentials.token)
+
+    assert info["email"] == credentials.service_account_email
+    info_scopes = _helpers.string_to_scopes(info["scope"])
+    assert set(info_scopes) == set(
+        [
+            "https://www.googleapis.com/auth/userinfo.email",
+            "https://www.googleapis.com/auth/userinfo.profile",
+        ]
+    )
+
+def test_iam_signer(http_request, credentials):
+    credentials = credentials.with_scopes(
+        ["https://www.googleapis.com/auth/iam"]
+    )
+
+    # Verify iamcredentials signer.
+    signer = iam.Signer(
+        http_request,
+        credentials,
+        credentials.service_account_email
+    )
+    
+    signed_blob = signer.sign("message")
+
+    assert isinstance(signed_blob, bytes)
diff --git a/system_tests/system_tests_sync/test_urllib3.py b/system_tests/system_tests_sync/test_urllib3.py
new file mode 100644
index 0000000..1932e19
--- /dev/null
+++ b/system_tests/system_tests_sync/test_urllib3.py
@@ -0,0 +1,44 @@
+# 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 google.auth
+import google.auth.credentials
+import google.auth.transport.requests
+from google.oauth2 import service_account
+
+
+def test_authorized_session_with_service_account_and_self_signed_jwt():
+    credentials, project_id = google.auth.default()
+
+    credentials = credentials.with_scopes(
+        scopes=[],
+        default_scopes=["https://www.googleapis.com/auth/pubsub"],
+    )
+
+    http = google.auth.transport.urllib3.AuthorizedHttp(
+        credentials=credentials, default_host="pubsub.googleapis.com"
+    )
+
+    # List Pub/Sub Topics through the REST API
+    # https://cloud.google.com/pubsub/docs/reference/rest/v1/projects.topics/list
+    response = http.urlopen(
+        method="GET",
+        url="https://pubsub.googleapis.com/v1/projects/{}/topics".format(project_id)
+    )
+
+    assert response.status == 200
+
+    # Check that self-signed JWT was created and is being used
+    assert credentials._jwt_credentials is not None
+    assert credentials._jwt_credentials.token == credentials.token
diff --git a/testing/constraints-2.7.txt b/testing/constraints-2.7.txt
new file mode 100644
index 0000000..dcc09f7
--- /dev/null
+++ b/testing/constraints-2.7.txt
@@ -0,0 +1 @@
+rsa==3.1.4
\ No newline at end of file
diff --git a/testing/constraints-3.10.txt b/testing/constraints-3.10.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/testing/constraints-3.10.txt
diff --git a/testing/constraints-3.11.txt b/testing/constraints-3.11.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/testing/constraints-3.11.txt
diff --git a/testing/constraints-3.6.txt b/testing/constraints-3.6.txt
new file mode 100644
index 0000000..6c4dd2e
--- /dev/null
+++ b/testing/constraints-3.6.txt
@@ -0,0 +1,13 @@
+# This constraints file is used to check that lower bounds
+# are correct in setup.py
+# List *all* library dependencies and extras in this file.
+# Pin the version to the lower bound.
+#
+# e.g., if setup.py has "foo >= 1.14.0, < 2.0.0dev",
+# Then this file should have foo==1.14.0
+cachetools==2.0.0
+pyasn1-modules==0.2.1
+setuptools==40.3.0
+rsa==3.1.4
+aiohttp==3.6.2
+requests==2.20.0
diff --git a/testing/constraints-3.7.txt b/testing/constraints-3.7.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/testing/constraints-3.7.txt
diff --git a/testing/constraints-3.8.txt b/testing/constraints-3.8.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/testing/constraints-3.8.txt
diff --git a/testing/constraints-3.9.txt b/testing/constraints-3.9.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/testing/constraints-3.9.txt
diff --git a/testing/requirements.txt b/testing/requirements.txt
new file mode 100644
index 0000000..df20f96
--- /dev/null
+++ b/testing/requirements.txt
@@ -0,0 +1,20 @@
+# Unit test requirements
+flask
+freezegun
+mock
+oauth2client
+pyopenssl
+pytest
+pytest-cov
+pytest-localserver
+pyu2f
+requests
+urllib3
+cryptography
+responses
+grpcio
+# Async Dependencies
+pytest-asyncio; python_version > '3.0'
+aioresponses; python_version > '3.0'
+asynctest; python_version > '3.0'
+aiohttp; python_version > '3.0'
\ No newline at end of file
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/__init__.py
diff --git a/tests/compute_engine/__init__.py b/tests/compute_engine/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/compute_engine/__init__.py
diff --git a/tests/compute_engine/test__metadata.py b/tests/compute_engine/test__metadata.py
new file mode 100644
index 0000000..852822d
--- /dev/null
+++ b/tests/compute_engine/test__metadata.py
@@ -0,0 +1,373 @@
+# Copyright 2016 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 datetime
+import json
+import os
+
+import mock
+import pytest
+from six.moves import http_client
+from six.moves import reload_module
+
+from google.auth import _helpers
+from google.auth import environment_vars
+from google.auth import exceptions
+from google.auth import transport
+from google.auth.compute_engine import _metadata
+
+PATH = "instance/service-accounts/default"
+
+
+def make_request(data, status=http_client.OK, headers=None, retry=False):
+    response = mock.create_autospec(transport.Response, instance=True)
+    response.status = status
+    response.data = _helpers.to_bytes(data)
+    response.headers = headers or {}
+
+    request = mock.create_autospec(transport.Request)
+    if retry:
+        request.side_effect = [exceptions.TransportError(), response]
+    else:
+        request.return_value = response
+
+    return request
+
+
+def test_ping_success():
+    request = make_request("", headers=_metadata._METADATA_HEADERS)
+
+    assert _metadata.ping(request)
+
+    request.assert_called_once_with(
+        method="GET",
+        url=_metadata._METADATA_IP_ROOT,
+        headers=_metadata._METADATA_HEADERS,
+        timeout=_metadata._METADATA_DEFAULT_TIMEOUT,
+    )
+
+
+def test_ping_success_retry():
+    request = make_request("", headers=_metadata._METADATA_HEADERS, retry=True)
+
+    assert _metadata.ping(request)
+
+    request.assert_called_with(
+        method="GET",
+        url=_metadata._METADATA_IP_ROOT,
+        headers=_metadata._METADATA_HEADERS,
+        timeout=_metadata._METADATA_DEFAULT_TIMEOUT,
+    )
+    assert request.call_count == 2
+
+
+def test_ping_failure_bad_flavor():
+    request = make_request("", headers={_metadata._METADATA_FLAVOR_HEADER: "meep"})
+
+    assert not _metadata.ping(request)
+
+
+def test_ping_failure_connection_failed():
+    request = make_request("")
+    request.side_effect = exceptions.TransportError()
+
+    assert not _metadata.ping(request)
+
+
+def test_ping_success_custom_root():
+    request = make_request("", headers=_metadata._METADATA_HEADERS)
+
+    fake_ip = "1.2.3.4"
+    os.environ[environment_vars.GCE_METADATA_IP] = fake_ip
+    reload_module(_metadata)
+
+    try:
+        assert _metadata.ping(request)
+    finally:
+        del os.environ[environment_vars.GCE_METADATA_IP]
+        reload_module(_metadata)
+
+    request.assert_called_once_with(
+        method="GET",
+        url="http://" + fake_ip,
+        headers=_metadata._METADATA_HEADERS,
+        timeout=_metadata._METADATA_DEFAULT_TIMEOUT,
+    )
+
+
+def test_get_success_json():
+    key, value = "foo", "bar"
+
+    data = json.dumps({key: value})
+    request = make_request(data, headers={"content-type": "application/json"})
+
+    result = _metadata.get(request, PATH)
+
+    request.assert_called_once_with(
+        method="GET",
+        url=_metadata._METADATA_ROOT + PATH,
+        headers=_metadata._METADATA_HEADERS,
+    )
+    assert result[key] == value
+
+
+def test_get_success_retry():
+    key, value = "foo", "bar"
+
+    data = json.dumps({key: value})
+    request = make_request(
+        data, headers={"content-type": "application/json"}, retry=True
+    )
+
+    result = _metadata.get(request, PATH)
+
+    request.assert_called_with(
+        method="GET",
+        url=_metadata._METADATA_ROOT + PATH,
+        headers=_metadata._METADATA_HEADERS,
+    )
+    assert request.call_count == 2
+    assert result[key] == value
+
+
+def test_get_success_text():
+    data = "foobar"
+    request = make_request(data, headers={"content-type": "text/plain"})
+
+    result = _metadata.get(request, PATH)
+
+    request.assert_called_once_with(
+        method="GET",
+        url=_metadata._METADATA_ROOT + PATH,
+        headers=_metadata._METADATA_HEADERS,
+    )
+    assert result == data
+
+
+def test_get_success_params():
+    data = "foobar"
+    request = make_request(data, headers={"content-type": "text/plain"})
+    params = {"recursive": "true"}
+
+    result = _metadata.get(request, PATH, params=params)
+
+    request.assert_called_once_with(
+        method="GET",
+        url=_metadata._METADATA_ROOT + PATH + "?recursive=true",
+        headers=_metadata._METADATA_HEADERS,
+    )
+    assert result == data
+
+
+def test_get_success_recursive_and_params():
+    data = "foobar"
+    request = make_request(data, headers={"content-type": "text/plain"})
+    params = {"recursive": "false"}
+    result = _metadata.get(request, PATH, recursive=True, params=params)
+
+    request.assert_called_once_with(
+        method="GET",
+        url=_metadata._METADATA_ROOT + PATH + "?recursive=true",
+        headers=_metadata._METADATA_HEADERS,
+    )
+    assert result == data
+
+
+def test_get_success_recursive():
+    data = "foobar"
+    request = make_request(data, headers={"content-type": "text/plain"})
+
+    result = _metadata.get(request, PATH, recursive=True)
+
+    request.assert_called_once_with(
+        method="GET",
+        url=_metadata._METADATA_ROOT + PATH + "?recursive=true",
+        headers=_metadata._METADATA_HEADERS,
+    )
+    assert result == data
+
+
+def test_get_success_custom_root_new_variable():
+    request = make_request("{}", headers={"content-type": "application/json"})
+
+    fake_root = "another.metadata.service"
+    os.environ[environment_vars.GCE_METADATA_HOST] = fake_root
+    reload_module(_metadata)
+
+    try:
+        _metadata.get(request, PATH)
+    finally:
+        del os.environ[environment_vars.GCE_METADATA_HOST]
+        reload_module(_metadata)
+
+    request.assert_called_once_with(
+        method="GET",
+        url="http://{}/computeMetadata/v1/{}".format(fake_root, PATH),
+        headers=_metadata._METADATA_HEADERS,
+    )
+
+
+def test_get_success_custom_root_old_variable():
+    request = make_request("{}", headers={"content-type": "application/json"})
+
+    fake_root = "another.metadata.service"
+    os.environ[environment_vars.GCE_METADATA_ROOT] = fake_root
+    reload_module(_metadata)
+
+    try:
+        _metadata.get(request, PATH)
+    finally:
+        del os.environ[environment_vars.GCE_METADATA_ROOT]
+        reload_module(_metadata)
+
+    request.assert_called_once_with(
+        method="GET",
+        url="http://{}/computeMetadata/v1/{}".format(fake_root, PATH),
+        headers=_metadata._METADATA_HEADERS,
+    )
+
+
+def test_get_failure():
+    request = make_request("Metadata error", status=http_client.NOT_FOUND)
+
+    with pytest.raises(exceptions.TransportError) as excinfo:
+        _metadata.get(request, PATH)
+
+    assert excinfo.match(r"Metadata error")
+
+    request.assert_called_once_with(
+        method="GET",
+        url=_metadata._METADATA_ROOT + PATH,
+        headers=_metadata._METADATA_HEADERS,
+    )
+
+
+def test_get_failure_connection_failed():
+    request = make_request("")
+    request.side_effect = exceptions.TransportError()
+
+    with pytest.raises(exceptions.TransportError) as excinfo:
+        _metadata.get(request, PATH)
+
+    assert excinfo.match(r"Compute Engine Metadata server unavailable")
+
+    request.assert_called_with(
+        method="GET",
+        url=_metadata._METADATA_ROOT + PATH,
+        headers=_metadata._METADATA_HEADERS,
+    )
+    assert request.call_count == 5
+
+
+def test_get_failure_bad_json():
+    request = make_request("{", headers={"content-type": "application/json"})
+
+    with pytest.raises(exceptions.TransportError) as excinfo:
+        _metadata.get(request, PATH)
+
+    assert excinfo.match(r"invalid JSON")
+
+    request.assert_called_once_with(
+        method="GET",
+        url=_metadata._METADATA_ROOT + PATH,
+        headers=_metadata._METADATA_HEADERS,
+    )
+
+
+def test_get_project_id():
+    project = "example-project"
+    request = make_request(project, headers={"content-type": "text/plain"})
+
+    project_id = _metadata.get_project_id(request)
+
+    request.assert_called_once_with(
+        method="GET",
+        url=_metadata._METADATA_ROOT + "project/project-id",
+        headers=_metadata._METADATA_HEADERS,
+    )
+    assert project_id == project
+
+
[email protected]("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
+def test_get_service_account_token(utcnow):
+    ttl = 500
+    request = make_request(
+        json.dumps({"access_token": "token", "expires_in": ttl}),
+        headers={"content-type": "application/json"},
+    )
+
+    token, expiry = _metadata.get_service_account_token(request)
+
+    request.assert_called_once_with(
+        method="GET",
+        url=_metadata._METADATA_ROOT + PATH + "/token",
+        headers=_metadata._METADATA_HEADERS,
+    )
+    assert token == "token"
+    assert expiry == utcnow() + datetime.timedelta(seconds=ttl)
+
+
[email protected]("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
+def test_get_service_account_token_with_scopes_list(utcnow):
+    ttl = 500
+    request = make_request(
+        json.dumps({"access_token": "token", "expires_in": ttl}),
+        headers={"content-type": "application/json"},
+    )
+
+    token, expiry = _metadata.get_service_account_token(request, scopes=["foo", "bar"])
+
+    request.assert_called_once_with(
+        method="GET",
+        url=_metadata._METADATA_ROOT + PATH + "/token" + "?scopes=foo%2Cbar",
+        headers=_metadata._METADATA_HEADERS,
+    )
+    assert token == "token"
+    assert expiry == utcnow() + datetime.timedelta(seconds=ttl)
+
+
[email protected]("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
+def test_get_service_account_token_with_scopes_string(utcnow):
+    ttl = 500
+    request = make_request(
+        json.dumps({"access_token": "token", "expires_in": ttl}),
+        headers={"content-type": "application/json"},
+    )
+
+    token, expiry = _metadata.get_service_account_token(request, scopes="foo,bar")
+
+    request.assert_called_once_with(
+        method="GET",
+        url=_metadata._METADATA_ROOT + PATH + "/token" + "?scopes=foo%2Cbar",
+        headers=_metadata._METADATA_HEADERS,
+    )
+    assert token == "token"
+    assert expiry == utcnow() + datetime.timedelta(seconds=ttl)
+
+
+def test_get_service_account_info():
+    key, value = "foo", "bar"
+    request = make_request(
+        json.dumps({key: value}), headers={"content-type": "application/json"}
+    )
+
+    info = _metadata.get_service_account_info(request)
+
+    request.assert_called_once_with(
+        method="GET",
+        url=_metadata._METADATA_ROOT + PATH + "/?recursive=true",
+        headers=_metadata._METADATA_HEADERS,
+    )
+
+    assert info[key] == value
diff --git a/tests/compute_engine/test_credentials.py b/tests/compute_engine/test_credentials.py
new file mode 100644
index 0000000..81cc6db
--- /dev/null
+++ b/tests/compute_engine/test_credentials.py
@@ -0,0 +1,798 @@
+# Copyright 2016 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 base64
+import datetime
+
+import mock
+import pytest
+import responses
+
+from google.auth import _helpers
+from google.auth import exceptions
+from google.auth import jwt
+from google.auth import transport
+from google.auth.compute_engine import credentials
+from google.auth.transport import requests
+
+SAMPLE_ID_TOKEN_EXP = 1584393400
+
+# header: {"alg": "RS256", "typ": "JWT", "kid": "1"}
+# payload: {"iss": "issuer", "iat": 1584393348, "sub": "subject",
+#   "exp": 1584393400,"aud": "audience"}
+SAMPLE_ID_TOKEN = (
+    b"eyJhbGciOiAiUlMyNTYiLCAidHlwIjogIkpXVCIsICJraWQiOiAiMSJ9."
+    b"eyJpc3MiOiAiaXNzdWVyIiwgImlhdCI6IDE1ODQzOTMzNDgsICJzdWIiO"
+    b"iAic3ViamVjdCIsICJleHAiOiAxNTg0MzkzNDAwLCAiYXVkIjogImF1ZG"
+    b"llbmNlIn0."
+    b"OquNjHKhTmlgCk361omRo18F_uY-7y0f_AmLbzW062Q1Zr61HAwHYP5FM"
+    b"316CK4_0cH8MUNGASsvZc3VqXAqub6PUTfhemH8pFEwBdAdG0LhrNkU0H"
+    b"WN1YpT55IiQ31esLdL5q-qDsOPpNZJUti1y1lAreM5nIn2srdWzGXGs4i"
+    b"TRQsn0XkNUCL4RErpciXmjfhMrPkcAjKA-mXQm2fa4jmTlEZFqFmUlym1"
+    b"ozJ0yf5grjN6AslN4OGvAv1pS-_Ko_pGBS6IQtSBC6vVKCUuBfaqNjykg"
+    b"bsxbLa6Fp0SYeYwO8ifEnkRvasVpc1WTQqfRB2JCj5pTBDzJpIpFCMmnQ"
+)
+
+
+class TestCredentials(object):
+    credentials = None
+
+    @pytest.fixture(autouse=True)
+    def credentials_fixture(self):
+        self.credentials = credentials.Credentials()
+
+    def test_default_state(self):
+        assert not self.credentials.valid
+        # Expiration hasn't been set yet
+        assert not self.credentials.expired
+        # Scopes are needed
+        assert self.credentials.requires_scopes
+        # Service account email hasn't been populated
+        assert self.credentials.service_account_email == "default"
+        # No quota project
+        assert not self.credentials._quota_project_id
+
+    @mock.patch(
+        "google.auth._helpers.utcnow",
+        return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD,
+    )
+    @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
+    def test_refresh_success(self, get, utcnow):
+        get.side_effect = [
+            {
+                # First request is for sevice account info.
+                "email": "[email protected]",
+                "scopes": ["one", "two"],
+            },
+            {
+                # Second request is for the token.
+                "access_token": "token",
+                "expires_in": 500,
+            },
+        ]
+
+        # Refresh credentials
+        self.credentials.refresh(None)
+
+        # Check that the credentials have the token and proper expiration
+        assert self.credentials.token == "token"
+        assert self.credentials.expiry == (utcnow() + datetime.timedelta(seconds=500))
+
+        # Check the credential info
+        assert self.credentials.service_account_email == "[email protected]"
+        assert self.credentials._scopes == ["one", "two"]
+
+        # Check that the credentials are valid (have a token and are not
+        # expired)
+        assert self.credentials.valid
+
+    @mock.patch(
+        "google.auth._helpers.utcnow",
+        return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD,
+    )
+    @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
+    def test_refresh_success_with_scopes(self, get, utcnow):
+        get.side_effect = [
+            {
+                # First request is for sevice account info.
+                "email": "[email protected]",
+                "scopes": ["one", "two"],
+            },
+            {
+                # Second request is for the token.
+                "access_token": "token",
+                "expires_in": 500,
+            },
+        ]
+
+        # Refresh credentials
+        scopes = ["three", "four"]
+        self.credentials = self.credentials.with_scopes(scopes)
+        self.credentials.refresh(None)
+
+        # Check that the credentials have the token and proper expiration
+        assert self.credentials.token == "token"
+        assert self.credentials.expiry == (utcnow() + datetime.timedelta(seconds=500))
+
+        # Check the credential info
+        assert self.credentials.service_account_email == "[email protected]"
+        assert self.credentials._scopes == scopes
+
+        # Check that the credentials are valid (have a token and are not
+        # expired)
+        assert self.credentials.valid
+
+        kwargs = get.call_args[1]
+        assert kwargs == {"params": {"scopes": "three,four"}}
+
+    @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
+    def test_refresh_error(self, get):
+        get.side_effect = exceptions.TransportError("http error")
+
+        with pytest.raises(exceptions.RefreshError) as excinfo:
+            self.credentials.refresh(None)
+
+        assert excinfo.match(r"http error")
+
+    @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
+    def test_before_request_refreshes(self, get):
+        get.side_effect = [
+            {
+                # First request is for sevice account info.
+                "email": "[email protected]",
+                "scopes": "one two",
+            },
+            {
+                # Second request is for the token.
+                "access_token": "token",
+                "expires_in": 500,
+            },
+        ]
+
+        # Credentials should start as invalid
+        assert not self.credentials.valid
+
+        # before_request should cause a refresh
+        request = mock.create_autospec(transport.Request, instance=True)
+        self.credentials.before_request(request, "GET", "http://example.com?a=1#3", {})
+
+        # The refresh endpoint should've been called.
+        assert get.called
+
+        # Credentials should now be valid.
+        assert self.credentials.valid
+
+    def test_with_quota_project(self):
+        quota_project_creds = self.credentials.with_quota_project("project-foo")
+
+        assert quota_project_creds._quota_project_id == "project-foo"
+
+    def test_with_scopes(self):
+        assert self.credentials._scopes is None
+
+        scopes = ["one", "two"]
+        self.credentials = self.credentials.with_scopes(scopes)
+
+        assert self.credentials._scopes == scopes
+
+
+class TestIDTokenCredentials(object):
+    credentials = None
+
+    @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
+    def test_default_state(self, get):
+        get.side_effect = [
+            {"email": "[email protected]", "scope": ["one", "two"]}
+        ]
+
+        request = mock.create_autospec(transport.Request, instance=True)
+        self.credentials = credentials.IDTokenCredentials(
+            request=request, target_audience="https://example.com"
+        )
+
+        assert not self.credentials.valid
+        # Expiration hasn't been set yet
+        assert not self.credentials.expired
+        # Service account email hasn't been populated
+        assert self.credentials.service_account_email == "[email protected]"
+        # Signer is initialized
+        assert self.credentials.signer
+        assert self.credentials.signer_email == "[email protected]"
+        # No quota project
+        assert not self.credentials._quota_project_id
+
+    @mock.patch(
+        "google.auth._helpers.utcnow",
+        return_value=datetime.datetime.utcfromtimestamp(0),
+    )
+    @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
+    @mock.patch("google.auth.iam.Signer.sign", autospec=True)
+    def test_make_authorization_grant_assertion(self, sign, get, utcnow):
+        get.side_effect = [
+            {"email": "[email protected]", "scopes": ["one", "two"]}
+        ]
+        sign.side_effect = [b"signature"]
+
+        request = mock.create_autospec(transport.Request, instance=True)
+        self.credentials = credentials.IDTokenCredentials(
+            request=request, target_audience="https://audience.com"
+        )
+
+        # Generate authorization grant:
+        token = self.credentials._make_authorization_grant_assertion()
+        payload = jwt.decode(token, verify=False)
+
+        # The JWT token signature is 'signature' encoded in base 64:
+        assert token.endswith(b".c2lnbmF0dXJl")
+
+        # Check that the credentials have the token and proper expiration
+        assert payload == {
+            "aud": "https://www.googleapis.com/oauth2/v4/token",
+            "exp": 3600,
+            "iat": 0,
+            "iss": "[email protected]",
+            "target_audience": "https://audience.com",
+        }
+
+    @mock.patch(
+        "google.auth._helpers.utcnow",
+        return_value=datetime.datetime.utcfromtimestamp(0),
+    )
+    @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
+    @mock.patch("google.auth.iam.Signer.sign", autospec=True)
+    def test_with_service_account(self, sign, get, utcnow):
+        sign.side_effect = [b"signature"]
+
+        request = mock.create_autospec(transport.Request, instance=True)
+        self.credentials = credentials.IDTokenCredentials(
+            request=request,
+            target_audience="https://audience.com",
+            service_account_email="[email protected]",
+        )
+
+        # Generate authorization grant:
+        token = self.credentials._make_authorization_grant_assertion()
+        payload = jwt.decode(token, verify=False)
+
+        # The JWT token signature is 'signature' encoded in base 64:
+        assert token.endswith(b".c2lnbmF0dXJl")
+
+        # Check that the credentials have the token and proper expiration
+        assert payload == {
+            "aud": "https://www.googleapis.com/oauth2/v4/token",
+            "exp": 3600,
+            "iat": 0,
+            "iss": "[email protected]",
+            "target_audience": "https://audience.com",
+        }
+
+    @mock.patch(
+        "google.auth._helpers.utcnow",
+        return_value=datetime.datetime.utcfromtimestamp(0),
+    )
+    @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
+    @mock.patch("google.auth.iam.Signer.sign", autospec=True)
+    def test_additional_claims(self, sign, get, utcnow):
+        get.side_effect = [
+            {"email": "[email protected]", "scopes": ["one", "two"]}
+        ]
+        sign.side_effect = [b"signature"]
+
+        request = mock.create_autospec(transport.Request, instance=True)
+        self.credentials = credentials.IDTokenCredentials(
+            request=request,
+            target_audience="https://audience.com",
+            additional_claims={"foo": "bar"},
+        )
+
+        # Generate authorization grant:
+        token = self.credentials._make_authorization_grant_assertion()
+        payload = jwt.decode(token, verify=False)
+
+        # The JWT token signature is 'signature' encoded in base 64:
+        assert token.endswith(b".c2lnbmF0dXJl")
+
+        # Check that the credentials have the token and proper expiration
+        assert payload == {
+            "aud": "https://www.googleapis.com/oauth2/v4/token",
+            "exp": 3600,
+            "iat": 0,
+            "iss": "[email protected]",
+            "target_audience": "https://audience.com",
+            "foo": "bar",
+        }
+
+    def test_token_uri(self):
+        request = mock.create_autospec(transport.Request, instance=True)
+
+        self.credentials = credentials.IDTokenCredentials(
+            request=request,
+            signer=mock.Mock(),
+            service_account_email="[email protected]",
+            target_audience="https://audience.com",
+        )
+        assert self.credentials._token_uri == credentials._DEFAULT_TOKEN_URI
+
+        self.credentials = credentials.IDTokenCredentials(
+            request=request,
+            signer=mock.Mock(),
+            service_account_email="[email protected]",
+            target_audience="https://audience.com",
+            token_uri="https://example.com/token",
+        )
+        assert self.credentials._token_uri == "https://example.com/token"
+
+    @mock.patch(
+        "google.auth._helpers.utcnow",
+        return_value=datetime.datetime.utcfromtimestamp(0),
+    )
+    @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
+    @mock.patch("google.auth.iam.Signer.sign", autospec=True)
+    def test_with_target_audience(self, sign, get, utcnow):
+        get.side_effect = [
+            {"email": "[email protected]", "scopes": ["one", "two"]}
+        ]
+        sign.side_effect = [b"signature"]
+
+        request = mock.create_autospec(transport.Request, instance=True)
+        self.credentials = credentials.IDTokenCredentials(
+            request=request, target_audience="https://audience.com"
+        )
+        self.credentials = self.credentials.with_target_audience("https://actually.not")
+
+        # Generate authorization grant:
+        token = self.credentials._make_authorization_grant_assertion()
+        payload = jwt.decode(token, verify=False)
+
+        # The JWT token signature is 'signature' encoded in base 64:
+        assert token.endswith(b".c2lnbmF0dXJl")
+
+        # Check that the credentials have the token and proper expiration
+        assert payload == {
+            "aud": "https://www.googleapis.com/oauth2/v4/token",
+            "exp": 3600,
+            "iat": 0,
+            "iss": "[email protected]",
+            "target_audience": "https://actually.not",
+        }
+
+        # Check that the signer have been initialized with a Request object
+        assert isinstance(self.credentials._signer._request, transport.Request)
+
+    @responses.activate
+    def test_with_target_audience_integration(self):
+        """ Test that it is possible to refresh credentials
+        generated from `with_target_audience`.
+
+        Instead of mocking the methods, the HTTP responses
+        have been mocked.
+        """
+
+        # mock information about credentials
+        responses.add(
+            responses.GET,
+            "http://metadata.google.internal/computeMetadata/v1/instance/"
+            "service-accounts/default/?recursive=true",
+            status=200,
+            content_type="application/json",
+            json={
+                "scopes": "email",
+                "email": "[email protected]",
+                "aliases": ["default"],
+            },
+        )
+
+        # mock token for credentials
+        responses.add(
+            responses.GET,
+            "http://metadata.google.internal/computeMetadata/v1/instance/"
+            "service-accounts/[email protected]/token",
+            status=200,
+            content_type="application/json",
+            json={
+                "access_token": "some-token",
+                "expires_in": 3210,
+                "token_type": "Bearer",
+            },
+        )
+
+        # mock sign blob endpoint
+        signature = base64.b64encode(b"some-signature").decode("utf-8")
+        responses.add(
+            responses.POST,
+            "https://iamcredentials.googleapis.com/v1/projects/-/"
+            "serviceAccounts/[email protected]:signBlob?alt=json",
+            status=200,
+            content_type="application/json",
+            json={"keyId": "some-key-id", "signedBlob": signature},
+        )
+
+        id_token = "{}.{}.{}".format(
+            base64.b64encode(b'{"some":"some"}').decode("utf-8"),
+            base64.b64encode(b'{"exp": 3210}').decode("utf-8"),
+            base64.b64encode(b"token").decode("utf-8"),
+        )
+
+        # mock id token endpoint
+        responses.add(
+            responses.POST,
+            "https://www.googleapis.com/oauth2/v4/token",
+            status=200,
+            content_type="application/json",
+            json={"id_token": id_token, "expiry": 3210},
+        )
+
+        self.credentials = credentials.IDTokenCredentials(
+            request=requests.Request(),
+            service_account_email="[email protected]",
+            target_audience="https://audience.com",
+        )
+
+        self.credentials = self.credentials.with_target_audience("https://actually.not")
+
+        self.credentials.refresh(requests.Request())
+
+        assert self.credentials.token is not None
+
+    @mock.patch(
+        "google.auth._helpers.utcnow",
+        return_value=datetime.datetime.utcfromtimestamp(0),
+    )
+    @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
+    @mock.patch("google.auth.iam.Signer.sign", autospec=True)
+    def test_with_quota_project(self, sign, get, utcnow):
+        get.side_effect = [
+            {"email": "[email protected]", "scopes": ["one", "two"]}
+        ]
+        sign.side_effect = [b"signature"]
+
+        request = mock.create_autospec(transport.Request, instance=True)
+        self.credentials = credentials.IDTokenCredentials(
+            request=request, target_audience="https://audience.com"
+        )
+        self.credentials = self.credentials.with_quota_project("project-foo")
+
+        assert self.credentials._quota_project_id == "project-foo"
+
+        # Generate authorization grant:
+        token = self.credentials._make_authorization_grant_assertion()
+        payload = jwt.decode(token, verify=False)
+
+        # The JWT token signature is 'signature' encoded in base 64:
+        assert token.endswith(b".c2lnbmF0dXJl")
+
+        # Check that the credentials have the token and proper expiration
+        assert payload == {
+            "aud": "https://www.googleapis.com/oauth2/v4/token",
+            "exp": 3600,
+            "iat": 0,
+            "iss": "[email protected]",
+            "target_audience": "https://audience.com",
+        }
+
+        # Check that the signer have been initialized with a Request object
+        assert isinstance(self.credentials._signer._request, transport.Request)
+
+    @responses.activate
+    def test_with_quota_project_integration(self):
+        """ Test that it is possible to refresh credentials
+        generated from `with_quota_project`.
+
+        Instead of mocking the methods, the HTTP responses
+        have been mocked.
+        """
+
+        # mock information about credentials
+        responses.add(
+            responses.GET,
+            "http://metadata.google.internal/computeMetadata/v1/instance/"
+            "service-accounts/default/?recursive=true",
+            status=200,
+            content_type="application/json",
+            json={
+                "scopes": "email",
+                "email": "[email protected]",
+                "aliases": ["default"],
+            },
+        )
+
+        # mock token for credentials
+        responses.add(
+            responses.GET,
+            "http://metadata.google.internal/computeMetadata/v1/instance/"
+            "service-accounts/[email protected]/token",
+            status=200,
+            content_type="application/json",
+            json={
+                "access_token": "some-token",
+                "expires_in": 3210,
+                "token_type": "Bearer",
+            },
+        )
+
+        # mock sign blob endpoint
+        signature = base64.b64encode(b"some-signature").decode("utf-8")
+        responses.add(
+            responses.POST,
+            "https://iamcredentials.googleapis.com/v1/projects/-/"
+            "serviceAccounts/[email protected]:signBlob?alt=json",
+            status=200,
+            content_type="application/json",
+            json={"keyId": "some-key-id", "signedBlob": signature},
+        )
+
+        id_token = "{}.{}.{}".format(
+            base64.b64encode(b'{"some":"some"}').decode("utf-8"),
+            base64.b64encode(b'{"exp": 3210}').decode("utf-8"),
+            base64.b64encode(b"token").decode("utf-8"),
+        )
+
+        # mock id token endpoint
+        responses.add(
+            responses.POST,
+            "https://www.googleapis.com/oauth2/v4/token",
+            status=200,
+            content_type="application/json",
+            json={"id_token": id_token, "expiry": 3210},
+        )
+
+        self.credentials = credentials.IDTokenCredentials(
+            request=requests.Request(),
+            service_account_email="[email protected]",
+            target_audience="https://audience.com",
+        )
+
+        self.credentials = self.credentials.with_quota_project("project-foo")
+
+        self.credentials.refresh(requests.Request())
+
+        assert self.credentials.token is not None
+        assert self.credentials._quota_project_id == "project-foo"
+
+    @mock.patch(
+        "google.auth._helpers.utcnow",
+        return_value=datetime.datetime.utcfromtimestamp(0),
+    )
+    @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
+    @mock.patch("google.auth.iam.Signer.sign", autospec=True)
+    @mock.patch("google.oauth2._client.id_token_jwt_grant", autospec=True)
+    def test_refresh_success(self, id_token_jwt_grant, sign, get, utcnow):
+        get.side_effect = [
+            {"email": "[email protected]", "scopes": ["one", "two"]}
+        ]
+        sign.side_effect = [b"signature"]
+        id_token_jwt_grant.side_effect = [
+            ("idtoken", datetime.datetime.utcfromtimestamp(3600), {})
+        ]
+
+        request = mock.create_autospec(transport.Request, instance=True)
+        self.credentials = credentials.IDTokenCredentials(
+            request=request, target_audience="https://audience.com"
+        )
+
+        # Refresh credentials
+        self.credentials.refresh(None)
+
+        # Check that the credentials have the token and proper expiration
+        assert self.credentials.token == "idtoken"
+        assert self.credentials.expiry == (datetime.datetime.utcfromtimestamp(3600))
+
+        # Check the credential info
+        assert self.credentials.service_account_email == "[email protected]"
+
+        # Check that the credentials are valid (have a token and are not
+        # expired)
+        assert self.credentials.valid
+
+    @mock.patch(
+        "google.auth._helpers.utcnow",
+        return_value=datetime.datetime.utcfromtimestamp(0),
+    )
+    @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
+    @mock.patch("google.auth.iam.Signer.sign", autospec=True)
+    def test_refresh_error(self, sign, get, utcnow):
+        get.side_effect = [
+            {"email": "[email protected]", "scopes": ["one", "two"]}
+        ]
+        sign.side_effect = [b"signature"]
+
+        request = mock.create_autospec(transport.Request, instance=True)
+        response = mock.Mock()
+        response.data = b'{"error": "http error"}'
+        response.status = 500
+        request.side_effect = [response]
+
+        self.credentials = credentials.IDTokenCredentials(
+            request=request, target_audience="https://audience.com"
+        )
+
+        with pytest.raises(exceptions.RefreshError) as excinfo:
+            self.credentials.refresh(request)
+
+        assert excinfo.match(r"http error")
+
+    @mock.patch(
+        "google.auth._helpers.utcnow",
+        return_value=datetime.datetime.utcfromtimestamp(0),
+    )
+    @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
+    @mock.patch("google.auth.iam.Signer.sign", autospec=True)
+    @mock.patch("google.oauth2._client.id_token_jwt_grant", autospec=True)
+    def test_before_request_refreshes(self, id_token_jwt_grant, sign, get, utcnow):
+        get.side_effect = [
+            {"email": "[email protected]", "scopes": "one two"}
+        ]
+        sign.side_effect = [b"signature"]
+        id_token_jwt_grant.side_effect = [
+            ("idtoken", datetime.datetime.utcfromtimestamp(3600), {})
+        ]
+
+        request = mock.create_autospec(transport.Request, instance=True)
+        self.credentials = credentials.IDTokenCredentials(
+            request=request, target_audience="https://audience.com"
+        )
+
+        # Credentials should start as invalid
+        assert not self.credentials.valid
+
+        # before_request should cause a refresh
+        request = mock.create_autospec(transport.Request, instance=True)
+        self.credentials.before_request(request, "GET", "http://example.com?a=1#3", {})
+
+        # The refresh endpoint should've been called.
+        assert get.called
+
+        # Credentials should now be valid.
+        assert self.credentials.valid
+
+    @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
+    @mock.patch("google.auth.iam.Signer.sign", autospec=True)
+    def test_sign_bytes(self, sign, get):
+        get.side_effect = [
+            {"email": "[email protected]", "scopes": ["one", "two"]}
+        ]
+        sign.side_effect = [b"signature"]
+
+        request = mock.create_autospec(transport.Request, instance=True)
+        response = mock.Mock()
+        response.data = b'{"signature": "c2lnbmF0dXJl"}'
+        response.status = 200
+        request.side_effect = [response]
+
+        self.credentials = credentials.IDTokenCredentials(
+            request=request, target_audience="https://audience.com"
+        )
+
+        # Generate authorization grant:
+        signature = self.credentials.sign_bytes(b"some bytes")
+
+        # The JWT token signature is 'signature' encoded in base 64:
+        assert signature == b"signature"
+
+    @mock.patch(
+        "google.auth.compute_engine._metadata.get_service_account_info", autospec=True
+    )
+    @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
+    def test_get_id_token_from_metadata(self, get, get_service_account_info):
+        get.return_value = SAMPLE_ID_TOKEN
+        get_service_account_info.return_value = {"email": "[email protected]"}
+
+        cred = credentials.IDTokenCredentials(
+            mock.Mock(), "audience", use_metadata_identity_endpoint=True
+        )
+        cred.refresh(request=mock.Mock())
+
+        assert cred.token == SAMPLE_ID_TOKEN
+        assert cred.expiry == datetime.datetime.fromtimestamp(SAMPLE_ID_TOKEN_EXP)
+        assert cred._use_metadata_identity_endpoint
+        assert cred._signer is None
+        assert cred._token_uri is None
+        assert cred._service_account_email == "[email protected]"
+        assert cred._target_audience == "audience"
+        with pytest.raises(ValueError):
+            cred.sign_bytes(b"bytes")
+
+    @mock.patch(
+        "google.auth.compute_engine._metadata.get_service_account_info", autospec=True
+    )
+    def test_with_target_audience_for_metadata(self, get_service_account_info):
+        get_service_account_info.return_value = {"email": "[email protected]"}
+
+        cred = credentials.IDTokenCredentials(
+            mock.Mock(), "audience", use_metadata_identity_endpoint=True
+        )
+        cred = cred.with_target_audience("new_audience")
+
+        assert cred._target_audience == "new_audience"
+        assert cred._use_metadata_identity_endpoint
+        assert cred._signer is None
+        assert cred._token_uri is None
+        assert cred._service_account_email == "[email protected]"
+
+    @mock.patch(
+        "google.auth.compute_engine._metadata.get_service_account_info", autospec=True
+    )
+    def test_id_token_with_quota_project(self, get_service_account_info):
+        get_service_account_info.return_value = {"email": "[email protected]"}
+
+        cred = credentials.IDTokenCredentials(
+            mock.Mock(), "audience", use_metadata_identity_endpoint=True
+        )
+        cred = cred.with_quota_project("project-foo")
+
+        assert cred._quota_project_id == "project-foo"
+        assert cred._use_metadata_identity_endpoint
+        assert cred._signer is None
+        assert cred._token_uri is None
+        assert cred._service_account_email == "[email protected]"
+
+    @mock.patch(
+        "google.auth.compute_engine._metadata.get_service_account_info", autospec=True
+    )
+    @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
+    def test_invalid_id_token_from_metadata(self, get, get_service_account_info):
+        get.return_value = "invalid_id_token"
+        get_service_account_info.return_value = {"email": "[email protected]"}
+
+        cred = credentials.IDTokenCredentials(
+            mock.Mock(), "audience", use_metadata_identity_endpoint=True
+        )
+
+        with pytest.raises(ValueError):
+            cred.refresh(request=mock.Mock())
+
+    @mock.patch(
+        "google.auth.compute_engine._metadata.get_service_account_info", autospec=True
+    )
+    @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
+    def test_transport_error_from_metadata(self, get, get_service_account_info):
+        get.side_effect = exceptions.TransportError("transport error")
+        get_service_account_info.return_value = {"email": "[email protected]"}
+
+        cred = credentials.IDTokenCredentials(
+            mock.Mock(), "audience", use_metadata_identity_endpoint=True
+        )
+
+        with pytest.raises(exceptions.RefreshError) as excinfo:
+            cred.refresh(request=mock.Mock())
+        assert excinfo.match(r"transport error")
+
+    def test_get_id_token_from_metadata_constructor(self):
+        with pytest.raises(ValueError):
+            credentials.IDTokenCredentials(
+                mock.Mock(),
+                "audience",
+                use_metadata_identity_endpoint=True,
+                token_uri="token_uri",
+            )
+        with pytest.raises(ValueError):
+            credentials.IDTokenCredentials(
+                mock.Mock(),
+                "audience",
+                use_metadata_identity_endpoint=True,
+                signer=mock.Mock(),
+            )
+        with pytest.raises(ValueError):
+            credentials.IDTokenCredentials(
+                mock.Mock(),
+                "audience",
+                use_metadata_identity_endpoint=True,
+                additional_claims={"key", "value"},
+            )
+        with pytest.raises(ValueError):
+            credentials.IDTokenCredentials(
+                mock.Mock(),
+                "audience",
+                use_metadata_identity_endpoint=True,
+                service_account_email="[email protected]",
+            )
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..cf8a0f9
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,49 @@
+# Copyright 2016 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 os
+import sys
+
+import mock
+import pytest
+
+
+def pytest_configure():
+    """Load public certificate and private key."""
+    pytest.data_dir = os.path.join(os.path.dirname(__file__), "data")
+
+    with open(os.path.join(pytest.data_dir, "privatekey.pem"), "rb") as fh:
+        pytest.private_key_bytes = fh.read()
+
+    with open(os.path.join(pytest.data_dir, "public_cert.pem"), "rb") as fh:
+        pytest.public_cert_bytes = fh.read()
+
+
[email protected]
+def mock_non_existent_module(monkeypatch):
+    """Mocks a non-existing module in sys.modules.
+
+    Additionally mocks any non-existing modules specified in the dotted path.
+    """
+
+    def _mock_non_existent_module(path):
+        parts = path.split(".")
+        partial = []
+        for part in parts:
+            partial.append(part)
+            current_module = ".".join(partial)
+            if current_module not in sys.modules:
+                monkeypatch.setitem(sys.modules, current_module, mock.MagicMock())
+
+    return _mock_non_existent_module
diff --git a/tests/crypt/__init__.py b/tests/crypt/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/crypt/__init__.py
diff --git a/tests/crypt/test__cryptography_rsa.py b/tests/crypt/test__cryptography_rsa.py
new file mode 100644
index 0000000..dbf07c7
--- /dev/null
+++ b/tests/crypt/test__cryptography_rsa.py
@@ -0,0 +1,161 @@
+# Copyright 2016 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 json
+import os
+
+from cryptography.hazmat.primitives.asymmetric import rsa
+import pytest
+
+from google.auth import _helpers
+from google.auth.crypt import _cryptography_rsa
+from google.auth.crypt import base
+
+
+DATA_DIR = os.path.join(os.path.dirname(__file__), "..", "data")
+
+# To generate privatekey.pem, privatekey.pub, and public_cert.pem:
+#   $ openssl req -new -newkey rsa:1024 -x509 -nodes -out public_cert.pem \
+#   >    -keyout privatekey.pem
+#   $ openssl rsa -in privatekey.pem -pubout -out privatekey.pub
+
+with open(os.path.join(DATA_DIR, "privatekey.pem"), "rb") as fh:
+    PRIVATE_KEY_BYTES = fh.read()
+    PKCS1_KEY_BYTES = PRIVATE_KEY_BYTES
+
+with open(os.path.join(DATA_DIR, "privatekey.pub"), "rb") as fh:
+    PUBLIC_KEY_BYTES = fh.read()
+
+with open(os.path.join(DATA_DIR, "public_cert.pem"), "rb") as fh:
+    PUBLIC_CERT_BYTES = fh.read()
+
+# To generate pem_from_pkcs12.pem and privatekey.p12:
+#   $ openssl pkcs12 -export -out privatekey.p12 -inkey privatekey.pem \
+#   >    -in public_cert.pem
+#   $ openssl pkcs12 -in privatekey.p12 -nocerts -nodes \
+#   >   -out pem_from_pkcs12.pem
+
+with open(os.path.join(DATA_DIR, "pem_from_pkcs12.pem"), "rb") as fh:
+    PKCS8_KEY_BYTES = fh.read()
+
+with open(os.path.join(DATA_DIR, "privatekey.p12"), "rb") as fh:
+    PKCS12_KEY_BYTES = fh.read()
+
+# The service account JSON file can be generated from the Google Cloud Console.
+SERVICE_ACCOUNT_JSON_FILE = os.path.join(DATA_DIR, "service_account.json")
+
+with open(SERVICE_ACCOUNT_JSON_FILE, "r") as fh:
+    SERVICE_ACCOUNT_INFO = json.load(fh)
+
+
+class TestRSAVerifier(object):
+    def test_verify_success(self):
+        to_sign = b"foo"
+        signer = _cryptography_rsa.RSASigner.from_string(PRIVATE_KEY_BYTES)
+        actual_signature = signer.sign(to_sign)
+
+        verifier = _cryptography_rsa.RSAVerifier.from_string(PUBLIC_KEY_BYTES)
+        assert verifier.verify(to_sign, actual_signature)
+
+    def test_verify_unicode_success(self):
+        to_sign = u"foo"
+        signer = _cryptography_rsa.RSASigner.from_string(PRIVATE_KEY_BYTES)
+        actual_signature = signer.sign(to_sign)
+
+        verifier = _cryptography_rsa.RSAVerifier.from_string(PUBLIC_KEY_BYTES)
+        assert verifier.verify(to_sign, actual_signature)
+
+    def test_verify_failure(self):
+        verifier = _cryptography_rsa.RSAVerifier.from_string(PUBLIC_KEY_BYTES)
+        bad_signature1 = b""
+        assert not verifier.verify(b"foo", bad_signature1)
+        bad_signature2 = b"a"
+        assert not verifier.verify(b"foo", bad_signature2)
+
+    def test_from_string_pub_key(self):
+        verifier = _cryptography_rsa.RSAVerifier.from_string(PUBLIC_KEY_BYTES)
+        assert isinstance(verifier, _cryptography_rsa.RSAVerifier)
+        assert isinstance(verifier._pubkey, rsa.RSAPublicKey)
+
+    def test_from_string_pub_key_unicode(self):
+        public_key = _helpers.from_bytes(PUBLIC_KEY_BYTES)
+        verifier = _cryptography_rsa.RSAVerifier.from_string(public_key)
+        assert isinstance(verifier, _cryptography_rsa.RSAVerifier)
+        assert isinstance(verifier._pubkey, rsa.RSAPublicKey)
+
+    def test_from_string_pub_cert(self):
+        verifier = _cryptography_rsa.RSAVerifier.from_string(PUBLIC_CERT_BYTES)
+        assert isinstance(verifier, _cryptography_rsa.RSAVerifier)
+        assert isinstance(verifier._pubkey, rsa.RSAPublicKey)
+
+    def test_from_string_pub_cert_unicode(self):
+        public_cert = _helpers.from_bytes(PUBLIC_CERT_BYTES)
+        verifier = _cryptography_rsa.RSAVerifier.from_string(public_cert)
+        assert isinstance(verifier, _cryptography_rsa.RSAVerifier)
+        assert isinstance(verifier._pubkey, rsa.RSAPublicKey)
+
+
+class TestRSASigner(object):
+    def test_from_string_pkcs1(self):
+        signer = _cryptography_rsa.RSASigner.from_string(PKCS1_KEY_BYTES)
+        assert isinstance(signer, _cryptography_rsa.RSASigner)
+        assert isinstance(signer._key, rsa.RSAPrivateKey)
+
+    def test_from_string_pkcs1_unicode(self):
+        key_bytes = _helpers.from_bytes(PKCS1_KEY_BYTES)
+        signer = _cryptography_rsa.RSASigner.from_string(key_bytes)
+        assert isinstance(signer, _cryptography_rsa.RSASigner)
+        assert isinstance(signer._key, rsa.RSAPrivateKey)
+
+    def test_from_string_pkcs8(self):
+        signer = _cryptography_rsa.RSASigner.from_string(PKCS8_KEY_BYTES)
+        assert isinstance(signer, _cryptography_rsa.RSASigner)
+        assert isinstance(signer._key, rsa.RSAPrivateKey)
+
+    def test_from_string_pkcs8_unicode(self):
+        key_bytes = _helpers.from_bytes(PKCS8_KEY_BYTES)
+        signer = _cryptography_rsa.RSASigner.from_string(key_bytes)
+        assert isinstance(signer, _cryptography_rsa.RSASigner)
+        assert isinstance(signer._key, rsa.RSAPrivateKey)
+
+    def test_from_string_pkcs12(self):
+        with pytest.raises(ValueError):
+            _cryptography_rsa.RSASigner.from_string(PKCS12_KEY_BYTES)
+
+    def test_from_string_bogus_key(self):
+        key_bytes = "bogus-key"
+        with pytest.raises(ValueError):
+            _cryptography_rsa.RSASigner.from_string(key_bytes)
+
+    def test_from_service_account_info(self):
+        signer = _cryptography_rsa.RSASigner.from_service_account_info(
+            SERVICE_ACCOUNT_INFO
+        )
+
+        assert signer.key_id == SERVICE_ACCOUNT_INFO[base._JSON_FILE_PRIVATE_KEY_ID]
+        assert isinstance(signer._key, rsa.RSAPrivateKey)
+
+    def test_from_service_account_info_missing_key(self):
+        with pytest.raises(ValueError) as excinfo:
+            _cryptography_rsa.RSASigner.from_service_account_info({})
+
+        assert excinfo.match(base._JSON_FILE_PRIVATE_KEY)
+
+    def test_from_service_account_file(self):
+        signer = _cryptography_rsa.RSASigner.from_service_account_file(
+            SERVICE_ACCOUNT_JSON_FILE
+        )
+
+        assert signer.key_id == SERVICE_ACCOUNT_INFO[base._JSON_FILE_PRIVATE_KEY_ID]
+        assert isinstance(signer._key, rsa.RSAPrivateKey)
diff --git a/tests/crypt/test__python_rsa.py b/tests/crypt/test__python_rsa.py
new file mode 100644
index 0000000..886ee55
--- /dev/null
+++ b/tests/crypt/test__python_rsa.py
@@ -0,0 +1,193 @@
+# Copyright 2016 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 json
+import os
+
+import mock
+from pyasn1_modules import pem
+import pytest
+import rsa
+import six
+
+from google.auth import _helpers
+from google.auth.crypt import _python_rsa
+from google.auth.crypt import base
+
+
+DATA_DIR = os.path.join(os.path.dirname(__file__), "..", "data")
+
+# To generate privatekey.pem, privatekey.pub, and public_cert.pem:
+#   $ openssl req -new -newkey rsa:1024 -x509 -nodes -out public_cert.pem \
+#   >    -keyout privatekey.pem
+#   $ openssl rsa -in privatekey.pem -pubout -out privatekey.pub
+
+with open(os.path.join(DATA_DIR, "privatekey.pem"), "rb") as fh:
+    PRIVATE_KEY_BYTES = fh.read()
+    PKCS1_KEY_BYTES = PRIVATE_KEY_BYTES
+
+with open(os.path.join(DATA_DIR, "privatekey.pub"), "rb") as fh:
+    PUBLIC_KEY_BYTES = fh.read()
+
+with open(os.path.join(DATA_DIR, "public_cert.pem"), "rb") as fh:
+    PUBLIC_CERT_BYTES = fh.read()
+
+# To generate pem_from_pkcs12.pem and privatekey.p12:
+#   $ openssl pkcs12 -export -out privatekey.p12 -inkey privatekey.pem \
+#   >    -in public_cert.pem
+#   $ openssl pkcs12 -in privatekey.p12 -nocerts -nodes \
+#   >   -out pem_from_pkcs12.pem
+
+with open(os.path.join(DATA_DIR, "pem_from_pkcs12.pem"), "rb") as fh:
+    PKCS8_KEY_BYTES = fh.read()
+
+with open(os.path.join(DATA_DIR, "privatekey.p12"), "rb") as fh:
+    PKCS12_KEY_BYTES = fh.read()
+
+# The service account JSON file can be generated from the Google Cloud Console.
+SERVICE_ACCOUNT_JSON_FILE = os.path.join(DATA_DIR, "service_account.json")
+
+with open(SERVICE_ACCOUNT_JSON_FILE, "r") as fh:
+    SERVICE_ACCOUNT_INFO = json.load(fh)
+
+
+class TestRSAVerifier(object):
+    def test_verify_success(self):
+        to_sign = b"foo"
+        signer = _python_rsa.RSASigner.from_string(PRIVATE_KEY_BYTES)
+        actual_signature = signer.sign(to_sign)
+
+        verifier = _python_rsa.RSAVerifier.from_string(PUBLIC_KEY_BYTES)
+        assert verifier.verify(to_sign, actual_signature)
+
+    def test_verify_unicode_success(self):
+        to_sign = u"foo"
+        signer = _python_rsa.RSASigner.from_string(PRIVATE_KEY_BYTES)
+        actual_signature = signer.sign(to_sign)
+
+        verifier = _python_rsa.RSAVerifier.from_string(PUBLIC_KEY_BYTES)
+        assert verifier.verify(to_sign, actual_signature)
+
+    def test_verify_failure(self):
+        verifier = _python_rsa.RSAVerifier.from_string(PUBLIC_KEY_BYTES)
+        bad_signature1 = b""
+        assert not verifier.verify(b"foo", bad_signature1)
+        bad_signature2 = b"a"
+        assert not verifier.verify(b"foo", bad_signature2)
+
+    def test_from_string_pub_key(self):
+        verifier = _python_rsa.RSAVerifier.from_string(PUBLIC_KEY_BYTES)
+        assert isinstance(verifier, _python_rsa.RSAVerifier)
+        assert isinstance(verifier._pubkey, rsa.key.PublicKey)
+
+    def test_from_string_pub_key_unicode(self):
+        public_key = _helpers.from_bytes(PUBLIC_KEY_BYTES)
+        verifier = _python_rsa.RSAVerifier.from_string(public_key)
+        assert isinstance(verifier, _python_rsa.RSAVerifier)
+        assert isinstance(verifier._pubkey, rsa.key.PublicKey)
+
+    def test_from_string_pub_cert(self):
+        verifier = _python_rsa.RSAVerifier.from_string(PUBLIC_CERT_BYTES)
+        assert isinstance(verifier, _python_rsa.RSAVerifier)
+        assert isinstance(verifier._pubkey, rsa.key.PublicKey)
+
+    def test_from_string_pub_cert_unicode(self):
+        public_cert = _helpers.from_bytes(PUBLIC_CERT_BYTES)
+        verifier = _python_rsa.RSAVerifier.from_string(public_cert)
+        assert isinstance(verifier, _python_rsa.RSAVerifier)
+        assert isinstance(verifier._pubkey, rsa.key.PublicKey)
+
+    def test_from_string_pub_cert_failure(self):
+        cert_bytes = PUBLIC_CERT_BYTES
+        true_der = rsa.pem.load_pem(cert_bytes, "CERTIFICATE")
+        load_pem_patch = mock.patch(
+            "rsa.pem.load_pem", return_value=true_der + b"extra", autospec=True
+        )
+
+        with load_pem_patch as load_pem:
+            with pytest.raises(ValueError):
+                _python_rsa.RSAVerifier.from_string(cert_bytes)
+            load_pem.assert_called_once_with(cert_bytes, "CERTIFICATE")
+
+
+class TestRSASigner(object):
+    def test_from_string_pkcs1(self):
+        signer = _python_rsa.RSASigner.from_string(PKCS1_KEY_BYTES)
+        assert isinstance(signer, _python_rsa.RSASigner)
+        assert isinstance(signer._key, rsa.key.PrivateKey)
+
+    def test_from_string_pkcs1_unicode(self):
+        key_bytes = _helpers.from_bytes(PKCS1_KEY_BYTES)
+        signer = _python_rsa.RSASigner.from_string(key_bytes)
+        assert isinstance(signer, _python_rsa.RSASigner)
+        assert isinstance(signer._key, rsa.key.PrivateKey)
+
+    def test_from_string_pkcs8(self):
+        signer = _python_rsa.RSASigner.from_string(PKCS8_KEY_BYTES)
+        assert isinstance(signer, _python_rsa.RSASigner)
+        assert isinstance(signer._key, rsa.key.PrivateKey)
+
+    def test_from_string_pkcs8_extra_bytes(self):
+        key_bytes = PKCS8_KEY_BYTES
+        _, pem_bytes = pem.readPemBlocksFromFile(
+            six.StringIO(_helpers.from_bytes(key_bytes)), _python_rsa._PKCS8_MARKER
+        )
+
+        key_info, remaining = None, "extra"
+        decode_patch = mock.patch(
+            "pyasn1.codec.der.decoder.decode",
+            return_value=(key_info, remaining),
+            autospec=True,
+        )
+
+        with decode_patch as decode:
+            with pytest.raises(ValueError):
+                _python_rsa.RSASigner.from_string(key_bytes)
+            # Verify mock was called.
+            decode.assert_called_once_with(pem_bytes, asn1Spec=_python_rsa._PKCS8_SPEC)
+
+    def test_from_string_pkcs8_unicode(self):
+        key_bytes = _helpers.from_bytes(PKCS8_KEY_BYTES)
+        signer = _python_rsa.RSASigner.from_string(key_bytes)
+        assert isinstance(signer, _python_rsa.RSASigner)
+        assert isinstance(signer._key, rsa.key.PrivateKey)
+
+    def test_from_string_pkcs12(self):
+        with pytest.raises(ValueError):
+            _python_rsa.RSASigner.from_string(PKCS12_KEY_BYTES)
+
+    def test_from_string_bogus_key(self):
+        key_bytes = "bogus-key"
+        with pytest.raises(ValueError):
+            _python_rsa.RSASigner.from_string(key_bytes)
+
+    def test_from_service_account_info(self):
+        signer = _python_rsa.RSASigner.from_service_account_info(SERVICE_ACCOUNT_INFO)
+
+        assert signer.key_id == SERVICE_ACCOUNT_INFO[base._JSON_FILE_PRIVATE_KEY_ID]
+        assert isinstance(signer._key, rsa.key.PrivateKey)
+
+    def test_from_service_account_info_missing_key(self):
+        with pytest.raises(ValueError) as excinfo:
+            _python_rsa.RSASigner.from_service_account_info({})
+
+        assert excinfo.match(base._JSON_FILE_PRIVATE_KEY)
+
+    def test_from_service_account_file(self):
+        signer = _python_rsa.RSASigner.from_service_account_file(
+            SERVICE_ACCOUNT_JSON_FILE
+        )
+
+        assert signer.key_id == SERVICE_ACCOUNT_INFO[base._JSON_FILE_PRIVATE_KEY_ID]
+        assert isinstance(signer._key, rsa.key.PrivateKey)
diff --git a/tests/crypt/test_crypt.py b/tests/crypt/test_crypt.py
new file mode 100644
index 0000000..e80502e
--- /dev/null
+++ b/tests/crypt/test_crypt.py
@@ -0,0 +1,58 @@
+# Copyright 2016 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 os
+
+from google.auth import crypt
+
+
+DATA_DIR = os.path.join(os.path.dirname(__file__), "..", "data")
+
+# To generate privatekey.pem, privatekey.pub, and public_cert.pem:
+#   $ openssl req -new -newkey rsa:1024 -x509 -nodes -out public_cert.pem \
+#   >    -keyout privatekey.pem
+#   $ openssl rsa -in privatekey.pem -pubout -out privatekey.pub
+
+with open(os.path.join(DATA_DIR, "privatekey.pem"), "rb") as fh:
+    PRIVATE_KEY_BYTES = fh.read()
+
+with open(os.path.join(DATA_DIR, "public_cert.pem"), "rb") as fh:
+    PUBLIC_CERT_BYTES = fh.read()
+
+# To generate other_cert.pem:
+#   $ openssl req -new -newkey rsa:1024 -x509 -nodes -out other_cert.pem
+
+with open(os.path.join(DATA_DIR, "other_cert.pem"), "rb") as fh:
+    OTHER_CERT_BYTES = fh.read()
+
+
+def test_verify_signature():
+    to_sign = b"foo"
+    signer = crypt.RSASigner.from_string(PRIVATE_KEY_BYTES)
+    signature = signer.sign(to_sign)
+
+    assert crypt.verify_signature(to_sign, signature, PUBLIC_CERT_BYTES)
+
+    # List of certs
+    assert crypt.verify_signature(
+        to_sign, signature, [OTHER_CERT_BYTES, PUBLIC_CERT_BYTES]
+    )
+
+
+def test_verify_signature_failure():
+    to_sign = b"foo"
+    signer = crypt.RSASigner.from_string(PRIVATE_KEY_BYTES)
+    signature = signer.sign(to_sign)
+
+    assert not crypt.verify_signature(to_sign, signature, OTHER_CERT_BYTES)
diff --git a/tests/crypt/test_es256.py b/tests/crypt/test_es256.py
new file mode 100644
index 0000000..5bb9050
--- /dev/null
+++ b/tests/crypt/test_es256.py
@@ -0,0 +1,143 @@
+# Copyright 2016 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import base64
+import json
+import os
+
+from cryptography.hazmat.primitives.asymmetric import ec
+import pytest
+
+from google.auth import _helpers
+from google.auth.crypt import base
+from google.auth.crypt import es256
+
+
+DATA_DIR = os.path.join(os.path.dirname(__file__), "..", "data")
+
+# To generate es256_privatekey.pem, es256_privatekey.pub, and
+# es256_public_cert.pem:
+#   $ openssl ecparam -genkey -name prime256v1 -noout -out es256_privatekey.pem
+#   $ openssl ec -in es256-private-key.pem -pubout -out es256-publickey.pem
+#   $ openssl req -new -x509 -key es256_privatekey.pem -out \
+#   >     es256_public_cert.pem
+
+with open(os.path.join(DATA_DIR, "es256_privatekey.pem"), "rb") as fh:
+    PRIVATE_KEY_BYTES = fh.read()
+    PKCS1_KEY_BYTES = PRIVATE_KEY_BYTES
+
+with open(os.path.join(DATA_DIR, "es256_publickey.pem"), "rb") as fh:
+    PUBLIC_KEY_BYTES = fh.read()
+
+with open(os.path.join(DATA_DIR, "es256_public_cert.pem"), "rb") as fh:
+    PUBLIC_CERT_BYTES = fh.read()
+
+SERVICE_ACCOUNT_JSON_FILE = os.path.join(DATA_DIR, "es256_service_account.json")
+
+with open(SERVICE_ACCOUNT_JSON_FILE, "r") as fh:
+    SERVICE_ACCOUNT_INFO = json.load(fh)
+
+
+class TestES256Verifier(object):
+    def test_verify_success(self):
+        to_sign = b"foo"
+        signer = es256.ES256Signer.from_string(PRIVATE_KEY_BYTES)
+        actual_signature = signer.sign(to_sign)
+
+        verifier = es256.ES256Verifier.from_string(PUBLIC_KEY_BYTES)
+        assert verifier.verify(to_sign, actual_signature)
+
+    def test_verify_unicode_success(self):
+        to_sign = u"foo"
+        signer = es256.ES256Signer.from_string(PRIVATE_KEY_BYTES)
+        actual_signature = signer.sign(to_sign)
+
+        verifier = es256.ES256Verifier.from_string(PUBLIC_KEY_BYTES)
+        assert verifier.verify(to_sign, actual_signature)
+
+    def test_verify_failure(self):
+        verifier = es256.ES256Verifier.from_string(PUBLIC_KEY_BYTES)
+        bad_signature1 = b""
+        assert not verifier.verify(b"foo", bad_signature1)
+        bad_signature2 = b"a"
+        assert not verifier.verify(b"foo", bad_signature2)
+
+    def test_verify_failure_with_wrong_raw_signature(self):
+        to_sign = b"foo"
+
+        # This signature has a wrong "r" value in the "(r,s)" raw signature.
+        wrong_signature = base64.urlsafe_b64decode(
+            b"m7oaRxUDeYqjZ8qiMwo0PZLTMZWKJLFQREpqce1StMIa_yXQQ-C5WgeIRHW7OqlYSDL0XbUrj_uAw9i-QhfOJQ=="
+        )
+
+        verifier = es256.ES256Verifier.from_string(PUBLIC_KEY_BYTES)
+        assert not verifier.verify(to_sign, wrong_signature)
+
+    def test_from_string_pub_key(self):
+        verifier = es256.ES256Verifier.from_string(PUBLIC_KEY_BYTES)
+        assert isinstance(verifier, es256.ES256Verifier)
+        assert isinstance(verifier._pubkey, ec.EllipticCurvePublicKey)
+
+    def test_from_string_pub_key_unicode(self):
+        public_key = _helpers.from_bytes(PUBLIC_KEY_BYTES)
+        verifier = es256.ES256Verifier.from_string(public_key)
+        assert isinstance(verifier, es256.ES256Verifier)
+        assert isinstance(verifier._pubkey, ec.EllipticCurvePublicKey)
+
+    def test_from_string_pub_cert(self):
+        verifier = es256.ES256Verifier.from_string(PUBLIC_CERT_BYTES)
+        assert isinstance(verifier, es256.ES256Verifier)
+        assert isinstance(verifier._pubkey, ec.EllipticCurvePublicKey)
+
+    def test_from_string_pub_cert_unicode(self):
+        public_cert = _helpers.from_bytes(PUBLIC_CERT_BYTES)
+        verifier = es256.ES256Verifier.from_string(public_cert)
+        assert isinstance(verifier, es256.ES256Verifier)
+        assert isinstance(verifier._pubkey, ec.EllipticCurvePublicKey)
+
+
+class TestES256Signer(object):
+    def test_from_string_pkcs1(self):
+        signer = es256.ES256Signer.from_string(PKCS1_KEY_BYTES)
+        assert isinstance(signer, es256.ES256Signer)
+        assert isinstance(signer._key, ec.EllipticCurvePrivateKey)
+
+    def test_from_string_pkcs1_unicode(self):
+        key_bytes = _helpers.from_bytes(PKCS1_KEY_BYTES)
+        signer = es256.ES256Signer.from_string(key_bytes)
+        assert isinstance(signer, es256.ES256Signer)
+        assert isinstance(signer._key, ec.EllipticCurvePrivateKey)
+
+    def test_from_string_bogus_key(self):
+        key_bytes = "bogus-key"
+        with pytest.raises(ValueError):
+            es256.ES256Signer.from_string(key_bytes)
+
+    def test_from_service_account_info(self):
+        signer = es256.ES256Signer.from_service_account_info(SERVICE_ACCOUNT_INFO)
+
+        assert signer.key_id == SERVICE_ACCOUNT_INFO[base._JSON_FILE_PRIVATE_KEY_ID]
+        assert isinstance(signer._key, ec.EllipticCurvePrivateKey)
+
+    def test_from_service_account_info_missing_key(self):
+        with pytest.raises(ValueError) as excinfo:
+            es256.ES256Signer.from_service_account_info({})
+
+        assert excinfo.match(base._JSON_FILE_PRIVATE_KEY)
+
+    def test_from_service_account_file(self):
+        signer = es256.ES256Signer.from_service_account_file(SERVICE_ACCOUNT_JSON_FILE)
+
+        assert signer.key_id == SERVICE_ACCOUNT_INFO[base._JSON_FILE_PRIVATE_KEY_ID]
+        assert isinstance(signer._key, ec.EllipticCurvePrivateKey)
diff --git a/tests/data/authorized_user.json b/tests/data/authorized_user.json
new file mode 100644
index 0000000..4787ace
--- /dev/null
+++ b/tests/data/authorized_user.json
@@ -0,0 +1,6 @@
+{
+  "client_id": "123",
+  "client_secret": "secret",
+  "refresh_token": "alabalaportocala",
+  "type": "authorized_user"
+}
diff --git a/tests/data/authorized_user_cloud_sdk.json b/tests/data/authorized_user_cloud_sdk.json
new file mode 100644
index 0000000..c9e19a6
--- /dev/null
+++ b/tests/data/authorized_user_cloud_sdk.json
@@ -0,0 +1,6 @@
+{
+  "client_id": "764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.apps.googleusercontent.com",
+  "client_secret": "secret",
+  "refresh_token": "alabalaportocala",
+  "type": "authorized_user"
+}
diff --git a/tests/data/authorized_user_cloud_sdk_with_quota_project_id.json b/tests/data/authorized_user_cloud_sdk_with_quota_project_id.json
new file mode 100644
index 0000000..53a8ff8
--- /dev/null
+++ b/tests/data/authorized_user_cloud_sdk_with_quota_project_id.json
@@ -0,0 +1,7 @@
+{
+  "client_id": "764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.apps.googleusercontent.com",
+  "client_secret": "secret",
+  "refresh_token": "alabalaportocala",
+  "type": "authorized_user",
+  "quota_project_id": "quota_project_id"
+}
diff --git a/tests/data/authorized_user_with_rapt_token.json b/tests/data/authorized_user_with_rapt_token.json
new file mode 100644
index 0000000..64b161d
--- /dev/null
+++ b/tests/data/authorized_user_with_rapt_token.json
@@ -0,0 +1,8 @@
+{
+    "client_id": "123",
+    "client_secret": "secret",
+    "refresh_token": "alabalaportocala",
+    "type": "authorized_user",
+    "rapt_token": "rapt"
+  }
+  
\ No newline at end of file
diff --git a/tests/data/client_secrets.json b/tests/data/client_secrets.json
new file mode 100644
index 0000000..1baa499
--- /dev/null
+++ b/tests/data/client_secrets.json
@@ -0,0 +1,14 @@
+{
+  "web": {
+    "client_id": "example.apps.googleusercontent.com",
+    "project_id": "example",
+    "auth_uri": "https://accounts.google.com/o/oauth2/auth",
+    "token_uri": "https://accounts.google.com/o/oauth2/token",
+    "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
+    "client_secret": "itsasecrettoeveryone",
+    "redirect_uris": [
+      "urn:ietf:wg:oauth:2.0:oob",
+      "http://localhost"
+    ]
+  }
+}
diff --git a/tests/data/cloud_sdk_config.json b/tests/data/cloud_sdk_config.json
new file mode 100644
index 0000000..a5fe4a9
--- /dev/null
+++ b/tests/data/cloud_sdk_config.json
@@ -0,0 +1,19 @@
+{
+  "configuration": {
+    "active_configuration": "default",
+    "properties": {
+      "core": {
+        "account": "[email protected]",
+        "disable_usage_reporting": "False",
+        "project": "example-project"
+      }
+    }
+  },
+  "credential": {
+    "access_token": "don't use me",
+    "token_expiry": "2017-03-23T23:09:49Z"
+  },
+  "sentinels": {
+    "config_sentinel": "/Users/example/.config/gcloud/config_sentinel"
+  }
+}
diff --git a/tests/data/context_aware_metadata.json b/tests/data/context_aware_metadata.json
new file mode 100644
index 0000000..ec40e78
--- /dev/null
+++ b/tests/data/context_aware_metadata.json
@@ -0,0 +1,6 @@
+{
+  "cert_provider_command":[
+    "/opt/google/endpoint-verification/bin/SecureConnectHelper",
+    "--print_certificate"],
+  "device_resource_ids":["11111111-1111-1111"]
+}
diff --git a/tests/data/es256_privatekey.pem b/tests/data/es256_privatekey.pem
new file mode 100644
index 0000000..5c950b5
--- /dev/null
+++ b/tests/data/es256_privatekey.pem
@@ -0,0 +1,5 @@
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIAIC57aTx5ev4T2HBMQk4fXV09AzLDQ3Ju1uNoEB0LngoAoGCCqGSM49
+AwEHoUQDQgAEsACsrmP6Bp216OCFm73C8W/VRHZWcO8yU/bMwx96f05BkTII3KeJ
+z2O0IRAnXfso8K6YsjMuUDGCfj+b1IDIoA==
+-----END EC PRIVATE KEY-----
diff --git a/tests/data/es256_public_cert.pem b/tests/data/es256_public_cert.pem
new file mode 100644
index 0000000..774ca14
--- /dev/null
+++ b/tests/data/es256_public_cert.pem
@@ -0,0 +1,8 @@
+-----BEGIN CERTIFICATE-----
+MIIBGDCBwAIJAPUA0H4EQWsdMAoGCCqGSM49BAMCMBUxEzARBgNVBAMMCnVuaXQt
+dGVzdHMwHhcNMTkwNTA5MDI1MDExWhcNMTkwNjA4MDI1MDExWjAVMRMwEQYDVQQD
+DAp1bml0LXRlc3RzMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEsACsrmP6Bp21
+6OCFm73C8W/VRHZWcO8yU/bMwx96f05BkTII3KeJz2O0IRAnXfso8K6YsjMuUDGC
+fj+b1IDIoDAKBggqhkjOPQQDAgNHADBEAh8PcDTMyWk8SHqV/v8FLuMbDxdtAsq2
+dwCpuHQwqCcmAiEAnwtkiyieN+8zozaf1P4QKp2mAqNGqua50y3ua5uVotc=
+-----END CERTIFICATE-----
diff --git a/tests/data/es256_publickey.pem b/tests/data/es256_publickey.pem
new file mode 100644
index 0000000..51f2a03
--- /dev/null
+++ b/tests/data/es256_publickey.pem
@@ -0,0 +1,4 @@
+-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEsACsrmP6Bp216OCFm73C8W/VRHZW
+cO8yU/bMwx96f05BkTII3KeJz2O0IRAnXfso8K6YsjMuUDGCfj+b1IDIoA==
+-----END PUBLIC KEY-----
diff --git a/tests/data/es256_service_account.json b/tests/data/es256_service_account.json
new file mode 100644
index 0000000..dd26719
--- /dev/null
+++ b/tests/data/es256_service_account.json
@@ -0,0 +1,10 @@
+{
+  "type": "service_account",
+  "project_id": "example-project",
+  "private_key_id": "1",
+  "private_key": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIAIC57aTx5ev4T2HBMQk4fXV09AzLDQ3Ju1uNoEB0LngoAoGCCqGSM49\nAwEHoUQDQgAEsACsrmP6Bp216OCFm73C8W/VRHZWcO8yU/bMwx96f05BkTII3KeJ\nz2O0IRAnXfso8K6YsjMuUDGCfj+b1IDIoA==\n-----END EC PRIVATE KEY-----",
+  "client_email": "[email protected]",
+  "client_id": "1234",
+  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
+  "token_uri": "https://accounts.google.com/o/oauth2/token"
+}
diff --git a/tests/data/external_subject_token.json b/tests/data/external_subject_token.json
new file mode 100644
index 0000000..a47ec34
--- /dev/null
+++ b/tests/data/external_subject_token.json
@@ -0,0 +1,3 @@
+{
+  "access_token": "HEADER.SIMULATED_JWT_PAYLOAD.SIGNATURE"
+}
\ No newline at end of file
diff --git a/tests/data/external_subject_token.txt b/tests/data/external_subject_token.txt
new file mode 100644
index 0000000..c668d8f
--- /dev/null
+++ b/tests/data/external_subject_token.txt
@@ -0,0 +1 @@
+HEADER.SIMULATED_JWT_PAYLOAD.SIGNATURE
\ No newline at end of file
diff --git a/tests/data/old_oauth_credentials_py3.pickle b/tests/data/old_oauth_credentials_py3.pickle
new file mode 100644
index 0000000..c8a0559
--- /dev/null
+++ b/tests/data/old_oauth_credentials_py3.pickle
Binary files differ
diff --git a/tests/data/other_cert.pem b/tests/data/other_cert.pem
new file mode 100644
index 0000000..6895d1e
--- /dev/null
+++ b/tests/data/other_cert.pem
@@ -0,0 +1,33 @@
+-----BEGIN CERTIFICATE-----
+MIIFtTCCA52gAwIBAgIJAPBsLZmNGfKtMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
+BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
+aWRnaXRzIFB0eSBMdGQwHhcNMTYwOTIxMDI0NTEyWhcNMTYxMDIxMDI0NTEyWjBF
+MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
+ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
+CgKCAgEAsiMC7mTsmUXwZoYlT4aHY1FLw8bxIXC+z3IqA+TY1WqfbeiZRo8MA5Zx
+lTTxYMKPCZUE1XBc7jvD8GJhWIj6pToPYHn73B01IBkLBxq4kF1yV2Z7DVmkvc6H
+EcxXXq8zkCx0j6XOfiI4+qkXnuQn8cvrk8xfhtnMMZM7iVm6VSN93iRP/8ey6xuL
+XTHrDX7ukoRce1hpT8O+15GXNrY0irhhYQz5xKibNCJF3EjV28WMry8y7I8uYUFU
+RWDiQawwK9ec1zhZ94v92+GZDlPevmcFmSERKYQ0NsKcT0Y3lGuGnaExs8GyOpnC
+oksu4YJGXQjg7lkv4MxzsNbRqmCkUwxw1Mg6FP0tsCNsw9qTrkvWCRA9zp/aU+sZ
+IBGh1t4UGCub8joeQFvHxvr/3F7mH/dyvCjA34u0Lo1VPx+jYUIi9i0odltMspDW
+xOpjqdGARZYmlJP5Au9q5cQjPMcwS/EBIb8cwNl32mUE6WnFlep+38mNR/FghIjO
+ViAkXuKQmcHe6xppZAoHFsO/t3l4Tjek5vNW7erI1rgrFku/fvkIW/G8V1yIm/+Q
+F+CE4maQzCJfhftpkhM/sPC/FuLNBmNE8BHVX8y58xG4is/cQxL4Z9TsFIw0C5+3
+uTrFW9D0agysahMVzPGtCqhDQqJdIJrBQqlS6bztpzBA8zEI0skCAwEAAaOBpzCB
+pDAdBgNVHQ4EFgQUz/8FmW6TfqXyNJZr7rhc+Tn5sKQwdQYDVR0jBG4wbIAUz/8F
+mW6TfqXyNJZr7rhc+Tn5sKShSaRHMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpT
+b21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGSCCQDw
+bC2ZjRnyrTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQCQmrcfhurX
+riR3Q0Y+nq040/3dJIAJXjyI9CEtxaU0nzCNTng7PwgZ0CKmCelQfInuwWFwBSHS
+6kBfC1rgJeFnjnTt8a3RCgRlIgUr9NCdPSEccB7TurobwPJ2h6cJjjR8urcb0CXh
+CEMvPneyPj0xUFY8vVKXMGWahz/kyfwIiVqcX/OtMZ29fUu1onbWl71g2gVLtUZl
+sECdZ+AC/6HDCVpYIVETMl1T7N/XyqXZQiDLDNRDeZhnapz8w9fsW1KVujAZLNQR
+pVnw2qa2UK1dSf2FHX+lQU5mFSYM4vtwaMlX/LgfdLZ9I796hFh619WwTVz+LO2N
+vHnwBMabld3XSPuZRqlbBulDQ07Vbqdjv8DYSLA2aKI4ZkMMKuFLG/oS28V2ZYmv
+/KpGEs5UgKY+P9NulYpTDwCU/6SomuQpP795wbG6sm7Hzq82r2RmB61GupNRGeqi
+pXKsy69T388zBxYu6zQrosXiDl5YzaViH7tm0J7opye8dCWjjpnahki0vq2znti7
+6cWla2j8Xz1glvLz+JI/NCOMfxUInb82T7ijo80N0VJ2hzf7p2GxRZXAxAV9knLI
+nM4F5TLjSd7ZhOOZ7ni/eZFueTMisWfypt2nc41whGjHMX/Zp1kPfhB4H2bLKIX/
+lSrwNr3qbGTEJX8JqpDBNVAd96XkMvDNyA==
+-----END CERTIFICATE-----
diff --git a/tests/data/pem_from_pkcs12.pem b/tests/data/pem_from_pkcs12.pem
new file mode 100644
index 0000000..2d77e10
--- /dev/null
+++ b/tests/data/pem_from_pkcs12.pem
@@ -0,0 +1,32 @@
+Bag Attributes
+    friendlyName: key
+    localKeyID: 22 7E 04 FC 64 48 20 83 1E C1 BD E3 F5 2F 44 7D EA 99 A5 BC 
+Key Attributes: <No Attributes>
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDh6PSnttDsv+vi
+tUZTP1E3hVBah6PUGDWZhYgNiyW8quTWCmPvBmCR2YzuhUrY5+CtKP8UJOQico+p
+oJHSAPsrzSr6YsGs3c9SQOslBmm9Fkh9/f/GZVTVZ6u5AsUmOcVvZ2q7Sz8Vj/aR
+aIm0EJqRe9cQ5vvN9sg25rIv4xKwIZJ1VixKWJLmpCmDINqn7xvl+ldlUmSr3aGt
+w21uSDuEJhQlzO3yf2FwJMkJ9SkCm9oVDXyl77OnKXj5bOQ/rojbyGeIxDJSUDWE
+GKyRPuqKi6rSbwg6h2G/Z9qBJkqM5NNTbGRIFz/9/LdmmwvtaqCxlLtD7RVEryAp
++qTGDk5hAgMBAAECggEBAMYYfNDEYpf4A2SdCLne/9zrrfZ0kphdUkL48MDPj5vN
+TzTRj6f9s5ixZ/+QKn3hdwbguCx13QbH5mocP0IjUhyqoFFHYAWxyyaZfpjM8tO4
+QoEYxby3BpjLe62UXESUzChQSytJZFwIDXKcdIPNO3zvVzufEJcfG5no2b9cIvsG
+Dy6J1FNILWxCtDIqBM+G1B1is9DhZnUDgn0iKzINiZmh1I1l7k/4tMnozVIKAfwo
+f1kYjG/d2IzDM02mTeTElz3IKeNriaOIYTZgI26xLJxTkiFnBV4JOWFAZw15X+yR
++DrjGSIkTfhzbLa20Vt3AFM+LFK0ZoXT2dRnjbYPjQECgYEA+9XJFGwLcEX6pl1p
+IwXAjXKJdju9DDn4lmHTW0Pbw25h1EXONwm/NPafwsWmPll9kW9IwsxUQVUyBC9a
+c3Q7rF1e8ai/qqVFRIZof275MI82ciV2Mw8Hz7FPAUyoju5CvnjAEH4+irt1VE/7
+SgdvQ1gDBQFegS69ijdz+cOhFxkCgYEA5aVoseMy/gIlsCvNPyw9+Jz/zBpKItX0
+jGzdF7lhERRO2cursujKaoHntRckHcE3P/Z4K565bvVq+VaVG0T/BcBKPmPHrLmY
+iuVXidltW7Jh9/RCVwb5+BvqlwlC470PEwhqoUatY/fPJ74srztrqJHvp1L29FT5
+sdmlJW8YwokCgYAUa3dMgp5C0knKp5RY1KSSU5E11w4zKZgwiWob4lq1dAPWtHpO
+GCo63yyBHImoUJVP75gUw4Cpc4EEudo5tlkIVuHV8nroGVKOhd9/Rb5K47Hke4kk
+Brn5a0Ues9qPDF65Fw1ryPDFSwHufjXAAO5SpZZJF51UGDgiNvDedbBgMQKBgHSk
+t7DjPhtW69234eCckD2fQS5ijBV1p2lMQmCygGM0dXiawvN02puOsCqDPoz+fxm2
+DwPY80cw0M0k9UeMnBxHt25JMDrDan/iTbxu++T/jlNrdebOXFlxlI5y3c7fULDS
+LZcNVzTXwhjlt7yp6d0NgzTyJw2ju9BiREfnTiRBAoGBAOPHrTOnPyjO+bVcCPTB
+WGLsbBd77mVPGIuL0XGrvbVYPE8yIcNbZcthd8VXL/38Ygy8SIZh2ZqsrU1b5WFa
+XUMLnGEODSS8x/GmW3i3KeirW5OxBNjfUzEF4XkJP8m41iTdsQEXQf9DdUY7X+CB
+VL5h7N0VstYhGgycuPpcIUQa
+-----END PRIVATE KEY-----
diff --git a/tests/data/privatekey.p12 b/tests/data/privatekey.p12
new file mode 100644
index 0000000..c369ecb
--- /dev/null
+++ b/tests/data/privatekey.p12
Binary files differ
diff --git a/tests/data/privatekey.pem b/tests/data/privatekey.pem
new file mode 100644
index 0000000..5744354
--- /dev/null
+++ b/tests/data/privatekey.pem
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj
+7wZgkdmM7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/
+xmVU1WeruQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYs
+SliS5qQpgyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18
+pe+zpyl4+WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xk
+SBc//fy3ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABAoIBAQDGGHzQxGKX+ANk
+nQi53v/c6632dJKYXVJC+PDAz4+bzU800Y+n/bOYsWf/kCp94XcG4Lgsdd0Gx+Zq
+HD9CI1IcqqBRR2AFscsmmX6YzPLTuEKBGMW8twaYy3utlFxElMwoUEsrSWRcCA1y
+nHSDzTt871c7nxCXHxuZ6Nm/XCL7Bg8uidRTSC1sQrQyKgTPhtQdYrPQ4WZ1A4J9
+IisyDYmZodSNZe5P+LTJ6M1SCgH8KH9ZGIxv3diMwzNNpk3kxJc9yCnja4mjiGE2
+YCNusSycU5IhZwVeCTlhQGcNeV/skfg64xkiJE34c2y2ttFbdwBTPixStGaF09nU
+Z422D40BAoGBAPvVyRRsC3BF+qZdaSMFwI1yiXY7vQw5+JZh01tD28NuYdRFzjcJ
+vzT2n8LFpj5ZfZFvSMLMVEFVMgQvWnN0O6xdXvGov6qlRUSGaH9u+TCPNnIldjMP
+B8+xTwFMqI7uQr54wBB+Poq7dVRP+0oHb0NYAwUBXoEuvYo3c/nDoRcZAoGBAOWl
+aLHjMv4CJbArzT8sPfic/8waSiLV9Ixs3Re5YREUTtnLq7LoymqB57UXJB3BNz/2
+eCueuW71avlWlRtE/wXASj5jx6y5mIrlV4nZbVuyYff0QlcG+fgb6pcJQuO9DxMI
+aqFGrWP3zye+LK87a6iR76dS9vRU+bHZpSVvGMKJAoGAFGt3TIKeQtJJyqeUWNSk
+klORNdcOMymYMIlqG+JatXQD1rR6ThgqOt8sgRyJqFCVT++YFMOAqXOBBLnaObZZ
+CFbh1fJ66BlSjoXff0W+SuOx5HuJJAa5+WtFHrPajwxeuRcNa8jwxUsB7n41wADu
+UqWWSRedVBg4Ijbw3nWwYDECgYB0pLew4z4bVuvdt+HgnJA9n0EuYowVdadpTEJg
+soBjNHV4msLzdNqbjrAqgz6M/n8Ztg8D2PNHMNDNJPVHjJwcR7duSTA6w2p/4k28
+bvvk/45Ta3XmzlxZcZSOct3O31Cw0i2XDVc018IY5be8qendDYM08icNo7vQYkRH
+504kQQKBgQDjx60zpz8ozvm1XAj0wVhi7GwXe+5lTxiLi9Fxq721WDxPMiHDW2XL
+YXfFVy/9/GIMvEiGYdmarK1NW+VhWl1DC5xhDg0kvMfxplt4tynoq1uTsQTY31Mx
+BeF5CT/JuNYk3bEBF0H/Q3VGO1/ggVS+YezdFbLWIRoMnLj6XCFEGg==
+-----END RSA PRIVATE KEY-----
diff --git a/tests/data/privatekey.pub b/tests/data/privatekey.pub
new file mode 100644
index 0000000..11fdaa4
--- /dev/null
+++ b/tests/data/privatekey.pub
@@ -0,0 +1,8 @@
+-----BEGIN RSA PUBLIC KEY-----
+MIIBCgKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj7wZg
+kdmM7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/xmVU
+1WeruQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYsSliS
+5qQpgyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18pe+z
+pyl4+WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xkSBc/
+/fy3ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQAB
+-----END RSA PUBLIC KEY-----
diff --git a/tests/data/public_cert.pem b/tests/data/public_cert.pem
new file mode 100644
index 0000000..7af6ca3
--- /dev/null
+++ b/tests/data/public_cert.pem
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDIzCCAgugAwIBAgIJAMfISuBQ5m+5MA0GCSqGSIb3DQEBBQUAMBUxEzARBgNV
+BAMTCnVuaXQtdGVzdHMwHhcNMTExMjA2MTYyNjAyWhcNMjExMjAzMTYyNjAyWjAV
+MRMwEQYDVQQDEwp1bml0LXRlc3RzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj7wZgkdmM
+7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/xmVU1Wer
+uQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYsSliS5qQp
+gyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18pe+zpyl4
++WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xkSBc//fy3
+ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABo3YwdDAdBgNVHQ4EFgQU2RQ8yO+O
+gN8oVW2SW7RLrfYd9jEwRQYDVR0jBD4wPIAU2RQ8yO+OgN8oVW2SW7RLrfYd9jGh
+GaQXMBUxEzARBgNVBAMTCnVuaXQtdGVzdHOCCQDHyErgUOZvuTAMBgNVHRMEBTAD
+AQH/MA0GCSqGSIb3DQEBBQUAA4IBAQBRv+M/6+FiVu7KXNjFI5pSN17OcW5QUtPr
+odJMlWrJBtynn/TA1oJlYu3yV5clc/71Vr/AxuX5xGP+IXL32YDF9lTUJXG/uUGk
++JETpKmQviPbRsvzYhz4pf6ZIOZMc3/GIcNq92ECbseGO+yAgyWUVKMmZM0HqXC9
+ovNslqe0M8C1sLm1zAR5z/h/litE7/8O2ietija3Q/qtl2TOXJdCA6sgjJX2WUql
+ybrC55ct18NKf3qhpcEkGQvFU40rVYApJpi98DiZPYFdx1oBDp/f4uZ3ojpxRVFT
+cDwcJLfNRCPUhormsY7fDS9xSyThiHsW9mjJYdcaKQkwYZ0F11yB
+-----END CERTIFICATE-----
diff --git a/tests/data/service_account.json b/tests/data/service_account.json
new file mode 100644
index 0000000..9e76f4d
--- /dev/null
+++ b/tests/data/service_account.json
@@ -0,0 +1,10 @@
+{
+  "type": "service_account",
+  "project_id": "example-project",
+  "private_key_id": "1",
+  "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj\n7wZgkdmM7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/\nxmVU1WeruQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYs\nSliS5qQpgyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18\npe+zpyl4+WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xk\nSBc//fy3ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABAoIBAQDGGHzQxGKX+ANk\nnQi53v/c6632dJKYXVJC+PDAz4+bzU800Y+n/bOYsWf/kCp94XcG4Lgsdd0Gx+Zq\nHD9CI1IcqqBRR2AFscsmmX6YzPLTuEKBGMW8twaYy3utlFxElMwoUEsrSWRcCA1y\nnHSDzTt871c7nxCXHxuZ6Nm/XCL7Bg8uidRTSC1sQrQyKgTPhtQdYrPQ4WZ1A4J9\nIisyDYmZodSNZe5P+LTJ6M1SCgH8KH9ZGIxv3diMwzNNpk3kxJc9yCnja4mjiGE2\nYCNusSycU5IhZwVeCTlhQGcNeV/skfg64xkiJE34c2y2ttFbdwBTPixStGaF09nU\nZ422D40BAoGBAPvVyRRsC3BF+qZdaSMFwI1yiXY7vQw5+JZh01tD28NuYdRFzjcJ\nvzT2n8LFpj5ZfZFvSMLMVEFVMgQvWnN0O6xdXvGov6qlRUSGaH9u+TCPNnIldjMP\nB8+xTwFMqI7uQr54wBB+Poq7dVRP+0oHb0NYAwUBXoEuvYo3c/nDoRcZAoGBAOWl\naLHjMv4CJbArzT8sPfic/8waSiLV9Ixs3Re5YREUTtnLq7LoymqB57UXJB3BNz/2\neCueuW71avlWlRtE/wXASj5jx6y5mIrlV4nZbVuyYff0QlcG+fgb6pcJQuO9DxMI\naqFGrWP3zye+LK87a6iR76dS9vRU+bHZpSVvGMKJAoGAFGt3TIKeQtJJyqeUWNSk\nklORNdcOMymYMIlqG+JatXQD1rR6ThgqOt8sgRyJqFCVT++YFMOAqXOBBLnaObZZ\nCFbh1fJ66BlSjoXff0W+SuOx5HuJJAa5+WtFHrPajwxeuRcNa8jwxUsB7n41wADu\nUqWWSRedVBg4Ijbw3nWwYDECgYB0pLew4z4bVuvdt+HgnJA9n0EuYowVdadpTEJg\nsoBjNHV4msLzdNqbjrAqgz6M/n8Ztg8D2PNHMNDNJPVHjJwcR7duSTA6w2p/4k28\nbvvk/45Ta3XmzlxZcZSOct3O31Cw0i2XDVc018IY5be8qendDYM08icNo7vQYkRH\n504kQQKBgQDjx60zpz8ozvm1XAj0wVhi7GwXe+5lTxiLi9Fxq721WDxPMiHDW2XL\nYXfFVy/9/GIMvEiGYdmarK1NW+VhWl1DC5xhDg0kvMfxplt4tynoq1uTsQTY31Mx\nBeF5CT/JuNYk3bEBF0H/Q3VGO1/ggVS+YezdFbLWIRoMnLj6XCFEGg==\n-----END RSA PRIVATE KEY-----\n",
+  "client_email": "[email protected]",
+  "client_id": "1234",
+  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
+  "token_uri": "https://accounts.google.com/o/oauth2/token"
+}
diff --git a/tests/oauth2/__init__.py b/tests/oauth2/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/oauth2/__init__.py
diff --git a/tests/oauth2/test__client.py b/tests/oauth2/test__client.py
new file mode 100644
index 0000000..54686df
--- /dev/null
+++ b/tests/oauth2/test__client.py
@@ -0,0 +1,329 @@
+# Copyright 2016 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 datetime
+import json
+import os
+
+import mock
+import pytest
+import six
+from six.moves import http_client
+from six.moves import urllib
+
+from google.auth import _helpers
+from google.auth import crypt
+from google.auth import exceptions
+from google.auth import jwt
+from google.auth import transport
+from google.oauth2 import _client
+
+
+DATA_DIR = os.path.join(os.path.dirname(__file__), "..", "data")
+
+with open(os.path.join(DATA_DIR, "privatekey.pem"), "rb") as fh:
+    PRIVATE_KEY_BYTES = fh.read()
+
+SIGNER = crypt.RSASigner.from_string(PRIVATE_KEY_BYTES, "1")
+
+SCOPES_AS_LIST = [
+    "https://www.googleapis.com/auth/pubsub",
+    "https://www.googleapis.com/auth/logging.write",
+]
+SCOPES_AS_STRING = (
+    "https://www.googleapis.com/auth/pubsub"
+    " https://www.googleapis.com/auth/logging.write"
+)
+
+
+def test__handle_error_response():
+    response_data = {"error": "help", "error_description": "I'm alive"}
+
+    with pytest.raises(exceptions.RefreshError) as excinfo:
+        _client._handle_error_response(response_data)
+
+    assert excinfo.match(r"help: I\'m alive")
+
+
+def test__handle_error_response_non_json():
+    response_data = {"foo": "bar"}
+
+    with pytest.raises(exceptions.RefreshError) as excinfo:
+        _client._handle_error_response(response_data)
+
+    assert excinfo.match(r"{\"foo\": \"bar\"}")
+
+
[email protected]("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
+def test__parse_expiry(unused_utcnow):
+    result = _client._parse_expiry({"expires_in": 500})
+    assert result == datetime.datetime.min + datetime.timedelta(seconds=500)
+
+
+def test__parse_expiry_none():
+    assert _client._parse_expiry({}) is None
+
+
+def make_request(response_data, status=http_client.OK):
+    response = mock.create_autospec(transport.Response, instance=True)
+    response.status = status
+    response.data = json.dumps(response_data).encode("utf-8")
+    request = mock.create_autospec(transport.Request)
+    request.return_value = response
+    return request
+
+
+def test__token_endpoint_request():
+    request = make_request({"test": "response"})
+
+    result = _client._token_endpoint_request(
+        request, "http://example.com", {"test": "params"}
+    )
+
+    # Check request call
+    request.assert_called_with(
+        method="POST",
+        url="http://example.com",
+        headers={"Content-Type": "application/x-www-form-urlencoded"},
+        body="test=params".encode("utf-8"),
+    )
+
+    # Check result
+    assert result == {"test": "response"}
+
+
+def test__token_endpoint_request_use_json():
+    request = make_request({"test": "response"})
+
+    result = _client._token_endpoint_request(
+        request,
+        "http://example.com",
+        {"test": "params"},
+        access_token="access_token",
+        use_json=True,
+    )
+
+    # Check request call
+    request.assert_called_with(
+        method="POST",
+        url="http://example.com",
+        headers={
+            "Content-Type": "application/json",
+            "Authorization": "Bearer access_token",
+        },
+        body=b'{"test": "params"}',
+    )
+
+    # Check result
+    assert result == {"test": "response"}
+
+
+def test__token_endpoint_request_error():
+    request = make_request({}, status=http_client.BAD_REQUEST)
+
+    with pytest.raises(exceptions.RefreshError):
+        _client._token_endpoint_request(request, "http://example.com", {})
+
+
+def test__token_endpoint_request_internal_failure_error():
+    request = make_request(
+        {"error_description": "internal_failure"}, status=http_client.BAD_REQUEST
+    )
+
+    with pytest.raises(exceptions.RefreshError):
+        _client._token_endpoint_request(
+            request, "http://example.com", {"error_description": "internal_failure"}
+        )
+
+    request = make_request(
+        {"error": "internal_failure"}, status=http_client.BAD_REQUEST
+    )
+
+    with pytest.raises(exceptions.RefreshError):
+        _client._token_endpoint_request(
+            request, "http://example.com", {"error": "internal_failure"}
+        )
+
+
+def verify_request_params(request, params):
+    request_body = request.call_args[1]["body"].decode("utf-8")
+    request_params = urllib.parse.parse_qs(request_body)
+
+    for key, value in six.iteritems(params):
+        assert request_params[key][0] == value
+
+
[email protected]("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
+def test_jwt_grant(utcnow):
+    request = make_request(
+        {"access_token": "token", "expires_in": 500, "extra": "data"}
+    )
+
+    token, expiry, extra_data = _client.jwt_grant(
+        request, "http://example.com", "assertion_value"
+    )
+
+    # Check request call
+    verify_request_params(
+        request, {"grant_type": _client._JWT_GRANT_TYPE, "assertion": "assertion_value"}
+    )
+
+    # Check result
+    assert token == "token"
+    assert expiry == utcnow() + datetime.timedelta(seconds=500)
+    assert extra_data["extra"] == "data"
+
+
+def test_jwt_grant_no_access_token():
+    request = make_request(
+        {
+            # No access token.
+            "expires_in": 500,
+            "extra": "data",
+        }
+    )
+
+    with pytest.raises(exceptions.RefreshError):
+        _client.jwt_grant(request, "http://example.com", "assertion_value")
+
+
+def test_id_token_jwt_grant():
+    now = _helpers.utcnow()
+    id_token_expiry = _helpers.datetime_to_secs(now)
+    id_token = jwt.encode(SIGNER, {"exp": id_token_expiry}).decode("utf-8")
+    request = make_request({"id_token": id_token, "extra": "data"})
+
+    token, expiry, extra_data = _client.id_token_jwt_grant(
+        request, "http://example.com", "assertion_value"
+    )
+
+    # Check request call
+    verify_request_params(
+        request, {"grant_type": _client._JWT_GRANT_TYPE, "assertion": "assertion_value"}
+    )
+
+    # Check result
+    assert token == id_token
+    # JWT does not store microseconds
+    now = now.replace(microsecond=0)
+    assert expiry == now
+    assert extra_data["extra"] == "data"
+
+
+def test_id_token_jwt_grant_no_access_token():
+    request = make_request(
+        {
+            # No access token.
+            "expires_in": 500,
+            "extra": "data",
+        }
+    )
+
+    with pytest.raises(exceptions.RefreshError):
+        _client.id_token_jwt_grant(request, "http://example.com", "assertion_value")
+
+
[email protected]("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
+def test_refresh_grant(unused_utcnow):
+    request = make_request(
+        {
+            "access_token": "token",
+            "refresh_token": "new_refresh_token",
+            "expires_in": 500,
+            "extra": "data",
+        }
+    )
+
+    token, refresh_token, expiry, extra_data = _client.refresh_grant(
+        request,
+        "http://example.com",
+        "refresh_token",
+        "client_id",
+        "client_secret",
+        rapt_token="rapt_token",
+    )
+
+    # Check request call
+    verify_request_params(
+        request,
+        {
+            "grant_type": _client._REFRESH_GRANT_TYPE,
+            "refresh_token": "refresh_token",
+            "client_id": "client_id",
+            "client_secret": "client_secret",
+            "rapt": "rapt_token",
+        },
+    )
+
+    # Check result
+    assert token == "token"
+    assert refresh_token == "new_refresh_token"
+    assert expiry == datetime.datetime.min + datetime.timedelta(seconds=500)
+    assert extra_data["extra"] == "data"
+
+
[email protected]("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
+def test_refresh_grant_with_scopes(unused_utcnow):
+    request = make_request(
+        {
+            "access_token": "token",
+            "refresh_token": "new_refresh_token",
+            "expires_in": 500,
+            "extra": "data",
+            "scope": SCOPES_AS_STRING,
+        }
+    )
+
+    token, refresh_token, expiry, extra_data = _client.refresh_grant(
+        request,
+        "http://example.com",
+        "refresh_token",
+        "client_id",
+        "client_secret",
+        SCOPES_AS_LIST,
+    )
+
+    # Check request call.
+    verify_request_params(
+        request,
+        {
+            "grant_type": _client._REFRESH_GRANT_TYPE,
+            "refresh_token": "refresh_token",
+            "client_id": "client_id",
+            "client_secret": "client_secret",
+            "scope": SCOPES_AS_STRING,
+        },
+    )
+
+    # Check result.
+    assert token == "token"
+    assert refresh_token == "new_refresh_token"
+    assert expiry == datetime.datetime.min + datetime.timedelta(seconds=500)
+    assert extra_data["extra"] == "data"
+
+
+def test_refresh_grant_no_access_token():
+    request = make_request(
+        {
+            # No access token.
+            "refresh_token": "new_refresh_token",
+            "expires_in": 500,
+            "extra": "data",
+        }
+    )
+
+    with pytest.raises(exceptions.RefreshError):
+        _client.refresh_grant(
+            request, "http://example.com", "refresh_token", "client_id", "client_secret"
+        )
diff --git a/tests/oauth2/test_challenges.py b/tests/oauth2/test_challenges.py
new file mode 100644
index 0000000..412895a
--- /dev/null
+++ b/tests/oauth2/test_challenges.py
@@ -0,0 +1,140 @@
+# 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.
+
+"""Tests for the reauth module."""
+
+import base64
+import sys
+
+import mock
+import pytest
+import pyu2f
+
+from google.auth import exceptions
+from google.oauth2 import challenges
+
+
+def test_get_user_password():
+    with mock.patch("getpass.getpass", return_value="foo"):
+        assert challenges.get_user_password("") == "foo"
+
+
+def test_security_key():
+    metadata = {
+        "status": "READY",
+        "challengeId": 2,
+        "challengeType": "SECURITY_KEY",
+        "securityKey": {
+            "applicationId": "security_key_application_id",
+            "challenges": [
+                {
+                    "keyHandle": "some_key",
+                    "challenge": base64.urlsafe_b64encode(
+                        "some_challenge".encode("ascii")
+                    ).decode("ascii"),
+                }
+            ],
+        },
+    }
+    mock_key = mock.Mock()
+
+    challenge = challenges.SecurityKeyChallenge()
+
+    # Test the case that security key challenge is passed.
+    with mock.patch("pyu2f.model.RegisteredKey", return_value=mock_key):
+        with mock.patch(
+            "pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate"
+        ) as mock_authenticate:
+            mock_authenticate.return_value = "security key response"
+            assert challenge.name == "SECURITY_KEY"
+            assert challenge.is_locally_eligible
+            assert challenge.obtain_challenge_input(metadata) == {
+                "securityKey": "security key response"
+            }
+            mock_authenticate.assert_called_with(
+                "security_key_application_id",
+                [{"key": mock_key, "challenge": b"some_challenge"}],
+                print_callback=sys.stderr.write,
+            )
+
+    # Test various types of exceptions.
+    with mock.patch("pyu2f.model.RegisteredKey", return_value=mock_key):
+        with mock.patch(
+            "pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate"
+        ) as mock_authenticate:
+            mock_authenticate.side_effect = pyu2f.errors.U2FError(
+                pyu2f.errors.U2FError.DEVICE_INELIGIBLE
+            )
+            assert challenge.obtain_challenge_input(metadata) is None
+
+        with mock.patch(
+            "pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate"
+        ) as mock_authenticate:
+            mock_authenticate.side_effect = pyu2f.errors.U2FError(
+                pyu2f.errors.U2FError.TIMEOUT
+            )
+            assert challenge.obtain_challenge_input(metadata) is None
+
+        with mock.patch(
+            "pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate"
+        ) as mock_authenticate:
+            mock_authenticate.side_effect = pyu2f.errors.U2FError(
+                pyu2f.errors.U2FError.BAD_REQUEST
+            )
+            with pytest.raises(pyu2f.errors.U2FError):
+                challenge.obtain_challenge_input(metadata)
+
+        with mock.patch(
+            "pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate"
+        ) as mock_authenticate:
+            mock_authenticate.side_effect = pyu2f.errors.NoDeviceFoundError()
+            assert challenge.obtain_challenge_input(metadata) is None
+
+        with mock.patch(
+            "pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate"
+        ) as mock_authenticate:
+            mock_authenticate.side_effect = pyu2f.errors.UnsupportedVersionException()
+            with pytest.raises(pyu2f.errors.UnsupportedVersionException):
+                challenge.obtain_challenge_input(metadata)
+
+        with mock.patch.dict("sys.modules"):
+            sys.modules["pyu2f"] = None
+            with pytest.raises(exceptions.ReauthFailError) as excinfo:
+                challenge.obtain_challenge_input(metadata)
+            assert excinfo.match(r"pyu2f dependency is required")
+
+
[email protected]("getpass.getpass", return_value="foo")
+def test_password_challenge(getpass_mock):
+    challenge = challenges.PasswordChallenge()
+
+    with mock.patch("getpass.getpass", return_value="foo"):
+        assert challenge.is_locally_eligible
+        assert challenge.name == "PASSWORD"
+        assert challenges.PasswordChallenge().obtain_challenge_input({}) == {
+            "credential": "foo"
+        }
+
+    with mock.patch("getpass.getpass", return_value=None):
+        assert challenges.PasswordChallenge().obtain_challenge_input({}) == {
+            "credential": " "
+        }
+
+
+def test_saml_challenge():
+    challenge = challenges.SamlChallenge()
+    assert challenge.is_locally_eligible
+    assert challenge.name == "SAML"
+    with pytest.raises(exceptions.ReauthSamlChallengeFailError):
+        challenge.obtain_challenge_input(None)
diff --git a/tests/oauth2/test_credentials.py b/tests/oauth2/test_credentials.py
new file mode 100644
index 0000000..243f97d
--- /dev/null
+++ b/tests/oauth2/test_credentials.py
@@ -0,0 +1,899 @@
+# Copyright 2016 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 datetime
+import json
+import os
+import pickle
+import sys
+
+import mock
+import pytest
+
+from google.auth import _helpers
+from google.auth import exceptions
+from google.auth import transport
+from google.oauth2 import credentials
+
+
+DATA_DIR = os.path.join(os.path.dirname(__file__), "..", "data")
+
+AUTH_USER_JSON_FILE = os.path.join(DATA_DIR, "authorized_user.json")
+
+with open(AUTH_USER_JSON_FILE, "r") as fh:
+    AUTH_USER_INFO = json.load(fh)
+
+
+class TestCredentials(object):
+    TOKEN_URI = "https://example.com/oauth2/token"
+    REFRESH_TOKEN = "refresh_token"
+    RAPT_TOKEN = "rapt_token"
+    CLIENT_ID = "client_id"
+    CLIENT_SECRET = "client_secret"
+
+    @classmethod
+    def make_credentials(cls):
+        return credentials.Credentials(
+            token=None,
+            refresh_token=cls.REFRESH_TOKEN,
+            token_uri=cls.TOKEN_URI,
+            client_id=cls.CLIENT_ID,
+            client_secret=cls.CLIENT_SECRET,
+            rapt_token=cls.RAPT_TOKEN,
+            enable_reauth_refresh=True,
+        )
+
+    def test_default_state(self):
+        credentials = self.make_credentials()
+        assert not credentials.valid
+        # Expiration hasn't been set yet
+        assert not credentials.expired
+        # Scopes aren't required for these credentials
+        assert not credentials.requires_scopes
+        # Test properties
+        assert credentials.refresh_token == self.REFRESH_TOKEN
+        assert credentials.token_uri == self.TOKEN_URI
+        assert credentials.client_id == self.CLIENT_ID
+        assert credentials.client_secret == self.CLIENT_SECRET
+        assert credentials.rapt_token == self.RAPT_TOKEN
+        assert credentials.refresh_handler is None
+
+    def test_refresh_handler_setter_and_getter(self):
+        scopes = ["email", "profile"]
+        original_refresh_handler = mock.Mock(return_value=("ACCESS_TOKEN_1", None))
+        updated_refresh_handler = mock.Mock(return_value=("ACCESS_TOKEN_2", None))
+        creds = credentials.Credentials(
+            token=None,
+            refresh_token=None,
+            token_uri=None,
+            client_id=None,
+            client_secret=None,
+            rapt_token=None,
+            scopes=scopes,
+            default_scopes=None,
+            refresh_handler=original_refresh_handler,
+        )
+
+        assert creds.refresh_handler is original_refresh_handler
+
+        creds.refresh_handler = updated_refresh_handler
+
+        assert creds.refresh_handler is updated_refresh_handler
+
+        creds.refresh_handler = None
+
+        assert creds.refresh_handler is None
+
+    def test_invalid_refresh_handler(self):
+        scopes = ["email", "profile"]
+        with pytest.raises(TypeError) as excinfo:
+            credentials.Credentials(
+                token=None,
+                refresh_token=None,
+                token_uri=None,
+                client_id=None,
+                client_secret=None,
+                rapt_token=None,
+                scopes=scopes,
+                default_scopes=None,
+                refresh_handler=object(),
+            )
+
+        assert excinfo.match("The provided refresh_handler is not a callable or None.")
+
+    @mock.patch("google.oauth2.reauth.refresh_grant", autospec=True)
+    @mock.patch(
+        "google.auth._helpers.utcnow",
+        return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD,
+    )
+    def test_refresh_success(self, unused_utcnow, refresh_grant):
+        token = "token"
+        new_rapt_token = "new_rapt_token"
+        expiry = _helpers.utcnow() + datetime.timedelta(seconds=500)
+        grant_response = {"id_token": mock.sentinel.id_token}
+        refresh_grant.return_value = (
+            # Access token
+            token,
+            # New refresh token
+            None,
+            # Expiry,
+            expiry,
+            # Extra data
+            grant_response,
+            # rapt_token
+            new_rapt_token,
+        )
+
+        request = mock.create_autospec(transport.Request)
+        credentials = self.make_credentials()
+
+        # Refresh credentials
+        credentials.refresh(request)
+
+        # Check jwt grant call.
+        refresh_grant.assert_called_with(
+            request,
+            self.TOKEN_URI,
+            self.REFRESH_TOKEN,
+            self.CLIENT_ID,
+            self.CLIENT_SECRET,
+            None,
+            self.RAPT_TOKEN,
+            True,
+        )
+
+        # Check that the credentials have the token and expiry
+        assert credentials.token == token
+        assert credentials.expiry == expiry
+        assert credentials.id_token == mock.sentinel.id_token
+        assert credentials.rapt_token == new_rapt_token
+
+        # Check that the credentials are valid (have a token and are not
+        # expired)
+        assert credentials.valid
+
+    def test_refresh_no_refresh_token(self):
+        request = mock.create_autospec(transport.Request)
+        credentials_ = credentials.Credentials(token=None, refresh_token=None)
+
+        with pytest.raises(exceptions.RefreshError, match="necessary fields"):
+            credentials_.refresh(request)
+
+        request.assert_not_called()
+
+    @mock.patch("google.oauth2.reauth.refresh_grant", autospec=True)
+    @mock.patch(
+        "google.auth._helpers.utcnow",
+        return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD,
+    )
+    def test_refresh_with_refresh_token_and_refresh_handler(
+        self, unused_utcnow, refresh_grant
+    ):
+        token = "token"
+        new_rapt_token = "new_rapt_token"
+        expiry = _helpers.utcnow() + datetime.timedelta(seconds=500)
+        grant_response = {"id_token": mock.sentinel.id_token}
+        refresh_grant.return_value = (
+            # Access token
+            token,
+            # New refresh token
+            None,
+            # Expiry,
+            expiry,
+            # Extra data
+            grant_response,
+            # rapt_token
+            new_rapt_token,
+        )
+
+        refresh_handler = mock.Mock()
+        request = mock.create_autospec(transport.Request)
+        creds = credentials.Credentials(
+            token=None,
+            refresh_token=self.REFRESH_TOKEN,
+            token_uri=self.TOKEN_URI,
+            client_id=self.CLIENT_ID,
+            client_secret=self.CLIENT_SECRET,
+            rapt_token=self.RAPT_TOKEN,
+            refresh_handler=refresh_handler,
+        )
+
+        # Refresh credentials
+        creds.refresh(request)
+
+        # Check jwt grant call.
+        refresh_grant.assert_called_with(
+            request,
+            self.TOKEN_URI,
+            self.REFRESH_TOKEN,
+            self.CLIENT_ID,
+            self.CLIENT_SECRET,
+            None,
+            self.RAPT_TOKEN,
+            False,
+        )
+
+        # Check that the credentials have the token and expiry
+        assert creds.token == token
+        assert creds.expiry == expiry
+        assert creds.id_token == mock.sentinel.id_token
+        assert creds.rapt_token == new_rapt_token
+
+        # Check that the credentials are valid (have a token and are not
+        # expired)
+        assert creds.valid
+
+        # Assert refresh handler not called as the refresh token has
+        # higher priority.
+        refresh_handler.assert_not_called()
+
+    @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
+    def test_refresh_with_refresh_handler_success_scopes(self, unused_utcnow):
+        expected_expiry = datetime.datetime.min + datetime.timedelta(seconds=2800)
+        refresh_handler = mock.Mock(return_value=("ACCESS_TOKEN", expected_expiry))
+        scopes = ["email", "profile"]
+        default_scopes = ["https://www.googleapis.com/auth/cloud-platform"]
+        request = mock.create_autospec(transport.Request)
+        creds = credentials.Credentials(
+            token=None,
+            refresh_token=None,
+            token_uri=None,
+            client_id=None,
+            client_secret=None,
+            rapt_token=None,
+            scopes=scopes,
+            default_scopes=default_scopes,
+            refresh_handler=refresh_handler,
+        )
+
+        creds.refresh(request)
+
+        assert creds.token == "ACCESS_TOKEN"
+        assert creds.expiry == expected_expiry
+        assert creds.valid
+        assert not creds.expired
+        # Confirm refresh handler called with the expected arguments.
+        refresh_handler.assert_called_with(request, scopes=scopes)
+
+    @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
+    def test_refresh_with_refresh_handler_success_default_scopes(self, unused_utcnow):
+        expected_expiry = datetime.datetime.min + datetime.timedelta(seconds=2800)
+        original_refresh_handler = mock.Mock(
+            return_value=("UNUSED_TOKEN", expected_expiry)
+        )
+        refresh_handler = mock.Mock(return_value=("ACCESS_TOKEN", expected_expiry))
+        default_scopes = ["https://www.googleapis.com/auth/cloud-platform"]
+        request = mock.create_autospec(transport.Request)
+        creds = credentials.Credentials(
+            token=None,
+            refresh_token=None,
+            token_uri=None,
+            client_id=None,
+            client_secret=None,
+            rapt_token=None,
+            scopes=None,
+            default_scopes=default_scopes,
+            refresh_handler=original_refresh_handler,
+        )
+
+        # Test newly set refresh_handler is used instead of the original one.
+        creds.refresh_handler = refresh_handler
+        creds.refresh(request)
+
+        assert creds.token == "ACCESS_TOKEN"
+        assert creds.expiry == expected_expiry
+        assert creds.valid
+        assert not creds.expired
+        # default_scopes should be used since no developer provided scopes
+        # are provided.
+        refresh_handler.assert_called_with(request, scopes=default_scopes)
+
+    @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
+    def test_refresh_with_refresh_handler_invalid_token(self, unused_utcnow):
+        expected_expiry = datetime.datetime.min + datetime.timedelta(seconds=2800)
+        # Simulate refresh handler does not return a valid token.
+        refresh_handler = mock.Mock(return_value=(None, expected_expiry))
+        scopes = ["email", "profile"]
+        default_scopes = ["https://www.googleapis.com/auth/cloud-platform"]
+        request = mock.create_autospec(transport.Request)
+        creds = credentials.Credentials(
+            token=None,
+            refresh_token=None,
+            token_uri=None,
+            client_id=None,
+            client_secret=None,
+            rapt_token=None,
+            scopes=scopes,
+            default_scopes=default_scopes,
+            refresh_handler=refresh_handler,
+        )
+
+        with pytest.raises(
+            exceptions.RefreshError, match="returned token is not a string"
+        ):
+            creds.refresh(request)
+
+        assert creds.token is None
+        assert creds.expiry is None
+        assert not creds.valid
+        # Confirm refresh handler called with the expected arguments.
+        refresh_handler.assert_called_with(request, scopes=scopes)
+
+    def test_refresh_with_refresh_handler_invalid_expiry(self):
+        # Simulate refresh handler returns expiration time in an invalid unit.
+        refresh_handler = mock.Mock(return_value=("TOKEN", 2800))
+        scopes = ["email", "profile"]
+        default_scopes = ["https://www.googleapis.com/auth/cloud-platform"]
+        request = mock.create_autospec(transport.Request)
+        creds = credentials.Credentials(
+            token=None,
+            refresh_token=None,
+            token_uri=None,
+            client_id=None,
+            client_secret=None,
+            rapt_token=None,
+            scopes=scopes,
+            default_scopes=default_scopes,
+            refresh_handler=refresh_handler,
+        )
+
+        with pytest.raises(
+            exceptions.RefreshError, match="returned expiry is not a datetime object"
+        ):
+            creds.refresh(request)
+
+        assert creds.token is None
+        assert creds.expiry is None
+        assert not creds.valid
+        # Confirm refresh handler called with the expected arguments.
+        refresh_handler.assert_called_with(request, scopes=scopes)
+
+    @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
+    def test_refresh_with_refresh_handler_expired_token(self, unused_utcnow):
+        expected_expiry = datetime.datetime.min + _helpers.REFRESH_THRESHOLD
+        # Simulate refresh handler returns an expired token.
+        refresh_handler = mock.Mock(return_value=("TOKEN", expected_expiry))
+        scopes = ["email", "profile"]
+        default_scopes = ["https://www.googleapis.com/auth/cloud-platform"]
+        request = mock.create_autospec(transport.Request)
+        creds = credentials.Credentials(
+            token=None,
+            refresh_token=None,
+            token_uri=None,
+            client_id=None,
+            client_secret=None,
+            rapt_token=None,
+            scopes=scopes,
+            default_scopes=default_scopes,
+            refresh_handler=refresh_handler,
+        )
+
+        with pytest.raises(exceptions.RefreshError, match="already expired"):
+            creds.refresh(request)
+
+        assert creds.token is None
+        assert creds.expiry is None
+        assert not creds.valid
+        # Confirm refresh handler called with the expected arguments.
+        refresh_handler.assert_called_with(request, scopes=scopes)
+
+    @mock.patch("google.oauth2.reauth.refresh_grant", autospec=True)
+    @mock.patch(
+        "google.auth._helpers.utcnow",
+        return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD,
+    )
+    def test_credentials_with_scopes_requested_refresh_success(
+        self, unused_utcnow, refresh_grant
+    ):
+        scopes = ["email", "profile"]
+        default_scopes = ["https://www.googleapis.com/auth/cloud-platform"]
+        token = "token"
+        new_rapt_token = "new_rapt_token"
+        expiry = _helpers.utcnow() + datetime.timedelta(seconds=500)
+        grant_response = {"id_token": mock.sentinel.id_token, "scope": "email profile"}
+        refresh_grant.return_value = (
+            # Access token
+            token,
+            # New refresh token
+            None,
+            # Expiry,
+            expiry,
+            # Extra data
+            grant_response,
+            # rapt token
+            new_rapt_token,
+        )
+
+        request = mock.create_autospec(transport.Request)
+        creds = credentials.Credentials(
+            token=None,
+            refresh_token=self.REFRESH_TOKEN,
+            token_uri=self.TOKEN_URI,
+            client_id=self.CLIENT_ID,
+            client_secret=self.CLIENT_SECRET,
+            scopes=scopes,
+            default_scopes=default_scopes,
+            rapt_token=self.RAPT_TOKEN,
+            enable_reauth_refresh=True,
+        )
+
+        # Refresh credentials
+        creds.refresh(request)
+
+        # Check jwt grant call.
+        refresh_grant.assert_called_with(
+            request,
+            self.TOKEN_URI,
+            self.REFRESH_TOKEN,
+            self.CLIENT_ID,
+            self.CLIENT_SECRET,
+            scopes,
+            self.RAPT_TOKEN,
+            True,
+        )
+
+        # Check that the credentials have the token and expiry
+        assert creds.token == token
+        assert creds.expiry == expiry
+        assert creds.id_token == mock.sentinel.id_token
+        assert creds.has_scopes(scopes)
+        assert creds.rapt_token == new_rapt_token
+
+        # Check that the credentials are valid (have a token and are not
+        # expired.)
+        assert creds.valid
+
+    @mock.patch("google.oauth2.reauth.refresh_grant", autospec=True)
+    @mock.patch(
+        "google.auth._helpers.utcnow",
+        return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD,
+    )
+    def test_credentials_with_only_default_scopes_requested(
+        self, unused_utcnow, refresh_grant
+    ):
+        default_scopes = ["email", "profile"]
+        token = "token"
+        new_rapt_token = "new_rapt_token"
+        expiry = _helpers.utcnow() + datetime.timedelta(seconds=500)
+        grant_response = {"id_token": mock.sentinel.id_token}
+        refresh_grant.return_value = (
+            # Access token
+            token,
+            # New refresh token
+            None,
+            # Expiry,
+            expiry,
+            # Extra data
+            grant_response,
+            # rapt token
+            new_rapt_token,
+        )
+
+        request = mock.create_autospec(transport.Request)
+        creds = credentials.Credentials(
+            token=None,
+            refresh_token=self.REFRESH_TOKEN,
+            token_uri=self.TOKEN_URI,
+            client_id=self.CLIENT_ID,
+            client_secret=self.CLIENT_SECRET,
+            default_scopes=default_scopes,
+            rapt_token=self.RAPT_TOKEN,
+            enable_reauth_refresh=True,
+        )
+
+        # Refresh credentials
+        creds.refresh(request)
+
+        # Check jwt grant call.
+        refresh_grant.assert_called_with(
+            request,
+            self.TOKEN_URI,
+            self.REFRESH_TOKEN,
+            self.CLIENT_ID,
+            self.CLIENT_SECRET,
+            default_scopes,
+            self.RAPT_TOKEN,
+            True,
+        )
+
+        # Check that the credentials have the token and expiry
+        assert creds.token == token
+        assert creds.expiry == expiry
+        assert creds.id_token == mock.sentinel.id_token
+        assert creds.has_scopes(default_scopes)
+        assert creds.rapt_token == new_rapt_token
+
+        # Check that the credentials are valid (have a token and are not
+        # expired.)
+        assert creds.valid
+
+    @mock.patch("google.oauth2.reauth.refresh_grant", autospec=True)
+    @mock.patch(
+        "google.auth._helpers.utcnow",
+        return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD,
+    )
+    def test_credentials_with_scopes_returned_refresh_success(
+        self, unused_utcnow, refresh_grant
+    ):
+        scopes = ["email", "profile"]
+        token = "token"
+        new_rapt_token = "new_rapt_token"
+        expiry = _helpers.utcnow() + datetime.timedelta(seconds=500)
+        grant_response = {
+            "id_token": mock.sentinel.id_token,
+            "scopes": " ".join(scopes),
+        }
+        refresh_grant.return_value = (
+            # Access token
+            token,
+            # New refresh token
+            None,
+            # Expiry,
+            expiry,
+            # Extra data
+            grant_response,
+            # rapt token
+            new_rapt_token,
+        )
+
+        request = mock.create_autospec(transport.Request)
+        creds = credentials.Credentials(
+            token=None,
+            refresh_token=self.REFRESH_TOKEN,
+            token_uri=self.TOKEN_URI,
+            client_id=self.CLIENT_ID,
+            client_secret=self.CLIENT_SECRET,
+            scopes=scopes,
+            rapt_token=self.RAPT_TOKEN,
+            enable_reauth_refresh=True,
+        )
+
+        # Refresh credentials
+        creds.refresh(request)
+
+        # Check jwt grant call.
+        refresh_grant.assert_called_with(
+            request,
+            self.TOKEN_URI,
+            self.REFRESH_TOKEN,
+            self.CLIENT_ID,
+            self.CLIENT_SECRET,
+            scopes,
+            self.RAPT_TOKEN,
+            True,
+        )
+
+        # Check that the credentials have the token and expiry
+        assert creds.token == token
+        assert creds.expiry == expiry
+        assert creds.id_token == mock.sentinel.id_token
+        assert creds.has_scopes(scopes)
+        assert creds.rapt_token == new_rapt_token
+
+        # Check that the credentials are valid (have a token and are not
+        # expired.)
+        assert creds.valid
+
+    @mock.patch("google.oauth2.reauth.refresh_grant", autospec=True)
+    @mock.patch(
+        "google.auth._helpers.utcnow",
+        return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD,
+    )
+    def test_credentials_with_scopes_refresh_failure_raises_refresh_error(
+        self, unused_utcnow, refresh_grant
+    ):
+        scopes = ["email", "profile"]
+        scopes_returned = ["email"]
+        token = "token"
+        new_rapt_token = "new_rapt_token"
+        expiry = _helpers.utcnow() + datetime.timedelta(seconds=500)
+        grant_response = {
+            "id_token": mock.sentinel.id_token,
+            "scope": " ".join(scopes_returned),
+        }
+        refresh_grant.return_value = (
+            # Access token
+            token,
+            # New refresh token
+            None,
+            # Expiry,
+            expiry,
+            # Extra data
+            grant_response,
+            # rapt token
+            new_rapt_token,
+        )
+
+        request = mock.create_autospec(transport.Request)
+        creds = credentials.Credentials(
+            token=None,
+            refresh_token=self.REFRESH_TOKEN,
+            token_uri=self.TOKEN_URI,
+            client_id=self.CLIENT_ID,
+            client_secret=self.CLIENT_SECRET,
+            scopes=scopes,
+            rapt_token=self.RAPT_TOKEN,
+            enable_reauth_refresh=True,
+        )
+
+        # Refresh credentials
+        with pytest.raises(
+            exceptions.RefreshError, match="Not all requested scopes were granted"
+        ):
+            creds.refresh(request)
+
+        # Check jwt grant call.
+        refresh_grant.assert_called_with(
+            request,
+            self.TOKEN_URI,
+            self.REFRESH_TOKEN,
+            self.CLIENT_ID,
+            self.CLIENT_SECRET,
+            scopes,
+            self.RAPT_TOKEN,
+            True,
+        )
+
+        # Check that the credentials have the token and expiry
+        assert creds.token == token
+        assert creds.expiry == expiry
+        assert creds.id_token == mock.sentinel.id_token
+        assert creds.has_scopes(scopes)
+        assert creds.rapt_token == new_rapt_token
+
+        # Check that the credentials are valid (have a token and are not
+        # expired.)
+        assert creds.valid
+
+    def test_apply_with_quota_project_id(self):
+        creds = credentials.Credentials(
+            token="token",
+            refresh_token=self.REFRESH_TOKEN,
+            token_uri=self.TOKEN_URI,
+            client_id=self.CLIENT_ID,
+            client_secret=self.CLIENT_SECRET,
+            quota_project_id="quota-project-123",
+        )
+
+        headers = {}
+        creds.apply(headers)
+        assert headers["x-goog-user-project"] == "quota-project-123"
+        assert "token" in headers["authorization"]
+
+    def test_apply_with_no_quota_project_id(self):
+        creds = credentials.Credentials(
+            token="token",
+            refresh_token=self.REFRESH_TOKEN,
+            token_uri=self.TOKEN_URI,
+            client_id=self.CLIENT_ID,
+            client_secret=self.CLIENT_SECRET,
+        )
+
+        headers = {}
+        creds.apply(headers)
+        assert "x-goog-user-project" not in headers
+        assert "token" in headers["authorization"]
+
+    def test_with_quota_project(self):
+        creds = credentials.Credentials(
+            token="token",
+            refresh_token=self.REFRESH_TOKEN,
+            token_uri=self.TOKEN_URI,
+            client_id=self.CLIENT_ID,
+            client_secret=self.CLIENT_SECRET,
+            quota_project_id="quota-project-123",
+        )
+
+        new_creds = creds.with_quota_project("new-project-456")
+        assert new_creds.quota_project_id == "new-project-456"
+        headers = {}
+        creds.apply(headers)
+        assert "x-goog-user-project" in headers
+
+    def test_from_authorized_user_info(self):
+        info = AUTH_USER_INFO.copy()
+
+        creds = credentials.Credentials.from_authorized_user_info(info)
+        assert creds.client_secret == info["client_secret"]
+        assert creds.client_id == info["client_id"]
+        assert creds.refresh_token == info["refresh_token"]
+        assert creds.token_uri == credentials._GOOGLE_OAUTH2_TOKEN_ENDPOINT
+        assert creds.scopes is None
+
+        scopes = ["email", "profile"]
+        creds = credentials.Credentials.from_authorized_user_info(info, scopes)
+        assert creds.client_secret == info["client_secret"]
+        assert creds.client_id == info["client_id"]
+        assert creds.refresh_token == info["refresh_token"]
+        assert creds.token_uri == credentials._GOOGLE_OAUTH2_TOKEN_ENDPOINT
+        assert creds.scopes == scopes
+
+        info["scopes"] = "email"  # single non-array scope from file
+        creds = credentials.Credentials.from_authorized_user_info(info)
+        assert creds.scopes == [info["scopes"]]
+
+        info["scopes"] = ["email", "profile"]  # array scope from file
+        creds = credentials.Credentials.from_authorized_user_info(info)
+        assert creds.scopes == info["scopes"]
+
+        expiry = datetime.datetime(2020, 8, 14, 15, 54, 1)
+        info["expiry"] = expiry.isoformat() + "Z"
+        creds = credentials.Credentials.from_authorized_user_info(info)
+        assert creds.expiry == expiry
+        assert creds.expired
+
+    def test_from_authorized_user_file(self):
+        info = AUTH_USER_INFO.copy()
+
+        creds = credentials.Credentials.from_authorized_user_file(AUTH_USER_JSON_FILE)
+        assert creds.client_secret == info["client_secret"]
+        assert creds.client_id == info["client_id"]
+        assert creds.refresh_token == info["refresh_token"]
+        assert creds.token_uri == credentials._GOOGLE_OAUTH2_TOKEN_ENDPOINT
+        assert creds.scopes is None
+        assert creds.rapt_token is None
+
+        scopes = ["email", "profile"]
+        creds = credentials.Credentials.from_authorized_user_file(
+            AUTH_USER_JSON_FILE, scopes
+        )
+        assert creds.client_secret == info["client_secret"]
+        assert creds.client_id == info["client_id"]
+        assert creds.refresh_token == info["refresh_token"]
+        assert creds.token_uri == credentials._GOOGLE_OAUTH2_TOKEN_ENDPOINT
+        assert creds.scopes == scopes
+
+    def test_from_authorized_user_file_with_rapt_token(self):
+        info = AUTH_USER_INFO.copy()
+        file_path = os.path.join(DATA_DIR, "authorized_user_with_rapt_token.json")
+
+        creds = credentials.Credentials.from_authorized_user_file(file_path)
+        assert creds.client_secret == info["client_secret"]
+        assert creds.client_id == info["client_id"]
+        assert creds.refresh_token == info["refresh_token"]
+        assert creds.token_uri == credentials._GOOGLE_OAUTH2_TOKEN_ENDPOINT
+        assert creds.scopes is None
+        assert creds.rapt_token == "rapt"
+
+    def test_to_json(self):
+        info = AUTH_USER_INFO.copy()
+        expiry = datetime.datetime(2020, 8, 14, 15, 54, 1)
+        info["expiry"] = expiry.isoformat() + "Z"
+        creds = credentials.Credentials.from_authorized_user_info(info)
+        assert creds.expiry == expiry
+
+        # Test with no `strip` arg
+        json_output = creds.to_json()
+        json_asdict = json.loads(json_output)
+        assert json_asdict.get("token") == creds.token
+        assert json_asdict.get("refresh_token") == creds.refresh_token
+        assert json_asdict.get("token_uri") == creds.token_uri
+        assert json_asdict.get("client_id") == creds.client_id
+        assert json_asdict.get("scopes") == creds.scopes
+        assert json_asdict.get("client_secret") == creds.client_secret
+        assert json_asdict.get("expiry") == info["expiry"]
+
+        # Test with a `strip` arg
+        json_output = creds.to_json(strip=["client_secret"])
+        json_asdict = json.loads(json_output)
+        assert json_asdict.get("token") == creds.token
+        assert json_asdict.get("refresh_token") == creds.refresh_token
+        assert json_asdict.get("token_uri") == creds.token_uri
+        assert json_asdict.get("client_id") == creds.client_id
+        assert json_asdict.get("scopes") == creds.scopes
+        assert json_asdict.get("client_secret") is None
+
+        # Test with no expiry
+        creds.expiry = None
+        json_output = creds.to_json()
+        json_asdict = json.loads(json_output)
+        assert json_asdict.get("expiry") is None
+
+    def test_pickle_and_unpickle(self):
+        creds = self.make_credentials()
+        unpickled = pickle.loads(pickle.dumps(creds))
+
+        # make sure attributes aren't lost during pickling
+        assert list(creds.__dict__).sort() == list(unpickled.__dict__).sort()
+
+        for attr in list(creds.__dict__):
+            assert getattr(creds, attr) == getattr(unpickled, attr)
+
+    def test_pickle_and_unpickle_with_refresh_handler(self):
+        expected_expiry = _helpers.utcnow() + datetime.timedelta(seconds=2800)
+        refresh_handler = mock.Mock(return_value=("TOKEN", expected_expiry))
+
+        creds = credentials.Credentials(
+            token=None,
+            refresh_token=None,
+            token_uri=None,
+            client_id=None,
+            client_secret=None,
+            rapt_token=None,
+            refresh_handler=refresh_handler,
+        )
+        unpickled = pickle.loads(pickle.dumps(creds))
+
+        # make sure attributes aren't lost during pickling
+        assert list(creds.__dict__).sort() == list(unpickled.__dict__).sort()
+
+        for attr in list(creds.__dict__):
+            # For the _refresh_handler property, the unpickled creds should be
+            # set to None.
+            if attr == "_refresh_handler":
+                assert getattr(unpickled, attr) is None
+            else:
+                assert getattr(creds, attr) == getattr(unpickled, attr)
+
+    def test_pickle_with_missing_attribute(self):
+        creds = self.make_credentials()
+
+        # remove an optional attribute before pickling
+        # this mimics a pickle created with a previous class definition with
+        # fewer attributes
+        del creds.__dict__["_quota_project_id"]
+
+        unpickled = pickle.loads(pickle.dumps(creds))
+
+        # Attribute should be initialized by `__setstate__`
+        assert unpickled.quota_project_id is None
+
+    # pickles are not compatible across versions
+    @pytest.mark.skipif(
+        sys.version_info < (3, 5),
+        reason="pickle file can only be loaded with Python >= 3.5",
+    )
+    def test_unpickle_old_credentials_pickle(self):
+        # make sure a credentials file pickled with an older
+        # library version (google-auth==1.5.1) can be unpickled
+        with open(
+            os.path.join(DATA_DIR, "old_oauth_credentials_py3.pickle"), "rb"
+        ) as f:
+            credentials = pickle.load(f)
+            assert credentials.quota_project_id is None
+
+
+class TestUserAccessTokenCredentials(object):
+    def test_instance(self):
+        cred = credentials.UserAccessTokenCredentials()
+        assert cred._account is None
+
+        cred = cred.with_account("account")
+        assert cred._account == "account"
+
+    @mock.patch("google.auth._cloud_sdk.get_auth_access_token", autospec=True)
+    def test_refresh(self, get_auth_access_token):
+        get_auth_access_token.return_value = "access_token"
+        cred = credentials.UserAccessTokenCredentials()
+        cred.refresh(None)
+        assert cred.token == "access_token"
+
+    def test_with_quota_project(self):
+        cred = credentials.UserAccessTokenCredentials()
+        quota_project_cred = cred.with_quota_project("project-foo")
+
+        assert quota_project_cred._quota_project_id == "project-foo"
+        assert quota_project_cred._account == cred._account
+
+    @mock.patch(
+        "google.oauth2.credentials.UserAccessTokenCredentials.apply", autospec=True
+    )
+    @mock.patch(
+        "google.oauth2.credentials.UserAccessTokenCredentials.refresh", autospec=True
+    )
+    def test_before_request(self, refresh, apply):
+        cred = credentials.UserAccessTokenCredentials()
+        cred.before_request(mock.Mock(), "GET", "https://example.com", {})
+        refresh.assert_called()
+        apply.assert_called()
diff --git a/tests/oauth2/test_id_token.py b/tests/oauth2/test_id_token.py
new file mode 100644
index 0000000..ccfaaaf
--- /dev/null
+++ b/tests/oauth2/test_id_token.py
@@ -0,0 +1,311 @@
+# Copyright 2014 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import json
+import os
+
+import mock
+import pytest
+
+from google.auth import environment_vars
+from google.auth import exceptions
+from google.auth import transport
+from google.oauth2 import id_token
+from google.oauth2 import service_account
+
+SERVICE_ACCOUNT_FILE = os.path.join(
+    os.path.dirname(__file__), "../data/service_account.json"
+)
+ID_TOKEN_AUDIENCE = "https://pubsub.googleapis.com"
+
+
+def make_request(status, data=None):
+    response = mock.create_autospec(transport.Response, instance=True)
+    response.status = status
+
+    if data is not None:
+        response.data = json.dumps(data).encode("utf-8")
+
+    request = mock.create_autospec(transport.Request)
+    request.return_value = response
+    return request
+
+
+def test__fetch_certs_success():
+    certs = {"1": "cert"}
+    request = make_request(200, certs)
+
+    returned_certs = id_token._fetch_certs(request, mock.sentinel.cert_url)
+
+    request.assert_called_once_with(mock.sentinel.cert_url, method="GET")
+    assert returned_certs == certs
+
+
+def test__fetch_certs_failure():
+    request = make_request(404)
+
+    with pytest.raises(exceptions.TransportError):
+        id_token._fetch_certs(request, mock.sentinel.cert_url)
+
+    request.assert_called_once_with(mock.sentinel.cert_url, method="GET")
+
+
[email protected]("google.auth.jwt.decode", autospec=True)
[email protected]("google.oauth2.id_token._fetch_certs", autospec=True)
+def test_verify_token(_fetch_certs, decode):
+    result = id_token.verify_token(mock.sentinel.token, mock.sentinel.request)
+
+    assert result == decode.return_value
+    _fetch_certs.assert_called_once_with(
+        mock.sentinel.request, id_token._GOOGLE_OAUTH2_CERTS_URL
+    )
+    decode.assert_called_once_with(
+        mock.sentinel.token,
+        certs=_fetch_certs.return_value,
+        audience=None,
+        clock_skew_in_seconds=0,
+    )
+
+
[email protected]("google.auth.jwt.decode", autospec=True)
[email protected]("google.oauth2.id_token._fetch_certs", autospec=True)
+def test_verify_token_args(_fetch_certs, decode):
+    result = id_token.verify_token(
+        mock.sentinel.token,
+        mock.sentinel.request,
+        audience=mock.sentinel.audience,
+        certs_url=mock.sentinel.certs_url,
+    )
+
+    assert result == decode.return_value
+    _fetch_certs.assert_called_once_with(mock.sentinel.request, mock.sentinel.certs_url)
+    decode.assert_called_once_with(
+        mock.sentinel.token,
+        certs=_fetch_certs.return_value,
+        audience=mock.sentinel.audience,
+        clock_skew_in_seconds=0,
+    )
+
+
[email protected]("google.auth.jwt.decode", autospec=True)
[email protected]("google.oauth2.id_token._fetch_certs", autospec=True)
+def test_verify_token_clock_skew(_fetch_certs, decode):
+    result = id_token.verify_token(
+        mock.sentinel.token,
+        mock.sentinel.request,
+        audience=mock.sentinel.audience,
+        certs_url=mock.sentinel.certs_url,
+        clock_skew_in_seconds=10,
+    )
+
+    assert result == decode.return_value
+    _fetch_certs.assert_called_once_with(mock.sentinel.request, mock.sentinel.certs_url)
+    decode.assert_called_once_with(
+        mock.sentinel.token,
+        certs=_fetch_certs.return_value,
+        audience=mock.sentinel.audience,
+        clock_skew_in_seconds=10,
+    )
+
+
[email protected]("google.oauth2.id_token.verify_token", autospec=True)
+def test_verify_oauth2_token(verify_token):
+    verify_token.return_value = {"iss": "accounts.google.com"}
+    result = id_token.verify_oauth2_token(
+        mock.sentinel.token, mock.sentinel.request, audience=mock.sentinel.audience
+    )
+
+    assert result == verify_token.return_value
+    verify_token.assert_called_once_with(
+        mock.sentinel.token,
+        mock.sentinel.request,
+        audience=mock.sentinel.audience,
+        certs_url=id_token._GOOGLE_OAUTH2_CERTS_URL,
+        clock_skew_in_seconds=0,
+    )
+
+
[email protected]("google.oauth2.id_token.verify_token", autospec=True)
+def test_verify_oauth2_token_clock_skew(verify_token):
+    verify_token.return_value = {"iss": "accounts.google.com"}
+    result = id_token.verify_oauth2_token(
+        mock.sentinel.token,
+        mock.sentinel.request,
+        audience=mock.sentinel.audience,
+        clock_skew_in_seconds=10,
+    )
+
+    assert result == verify_token.return_value
+    verify_token.assert_called_once_with(
+        mock.sentinel.token,
+        mock.sentinel.request,
+        audience=mock.sentinel.audience,
+        certs_url=id_token._GOOGLE_OAUTH2_CERTS_URL,
+        clock_skew_in_seconds=10,
+    )
+
+
[email protected]("google.oauth2.id_token.verify_token", autospec=True)
+def test_verify_oauth2_token_invalid_iss(verify_token):
+    verify_token.return_value = {"iss": "invalid_issuer"}
+
+    with pytest.raises(exceptions.GoogleAuthError):
+        id_token.verify_oauth2_token(
+            mock.sentinel.token, mock.sentinel.request, audience=mock.sentinel.audience
+        )
+
+
[email protected]("google.oauth2.id_token.verify_token", autospec=True)
+def test_verify_firebase_token(verify_token):
+    result = id_token.verify_firebase_token(
+        mock.sentinel.token, mock.sentinel.request, audience=mock.sentinel.audience
+    )
+
+    assert result == verify_token.return_value
+    verify_token.assert_called_once_with(
+        mock.sentinel.token,
+        mock.sentinel.request,
+        audience=mock.sentinel.audience,
+        certs_url=id_token._GOOGLE_APIS_CERTS_URL,
+        clock_skew_in_seconds=0,
+    )
+
+
[email protected]("google.oauth2.id_token.verify_token", autospec=True)
+def test_verify_firebase_token_clock_skew(verify_token):
+    result = id_token.verify_firebase_token(
+        mock.sentinel.token,
+        mock.sentinel.request,
+        audience=mock.sentinel.audience,
+        clock_skew_in_seconds=10,
+    )
+
+    assert result == verify_token.return_value
+    verify_token.assert_called_once_with(
+        mock.sentinel.token,
+        mock.sentinel.request,
+        audience=mock.sentinel.audience,
+        certs_url=id_token._GOOGLE_APIS_CERTS_URL,
+        clock_skew_in_seconds=10,
+    )
+
+
+def test_fetch_id_token_credentials_optional_request(monkeypatch):
+    monkeypatch.delenv(environment_vars.CREDENTIALS, raising=False)
+
+    # Test a request object is created if not provided
+    with mock.patch("google.auth.compute_engine._metadata.ping", return_value=True):
+        with mock.patch(
+            "google.auth.compute_engine.IDTokenCredentials.__init__", return_value=None
+        ):
+            with mock.patch(
+                "google.auth.transport.requests.Request.__init__", return_value=None
+            ) as mock_request:
+                id_token.fetch_id_token_credentials(ID_TOKEN_AUDIENCE)
+            mock_request.assert_called()
+
+
+def test_fetch_id_token_credentials_from_metadata_server(monkeypatch):
+    monkeypatch.delenv(environment_vars.CREDENTIALS, raising=False)
+
+    mock_req = mock.Mock()
+
+    with mock.patch("google.auth.compute_engine._metadata.ping", return_value=True):
+        with mock.patch(
+            "google.auth.compute_engine.IDTokenCredentials.__init__", return_value=None
+        ) as mock_init:
+            id_token.fetch_id_token_credentials(ID_TOKEN_AUDIENCE, request=mock_req)
+        mock_init.assert_called_once_with(
+            mock_req, ID_TOKEN_AUDIENCE, use_metadata_identity_endpoint=True
+        )
+
+
+def test_fetch_id_token_credentials_from_explicit_cred_json_file(monkeypatch):
+    monkeypatch.setenv(environment_vars.CREDENTIALS, SERVICE_ACCOUNT_FILE)
+
+    cred = id_token.fetch_id_token_credentials(ID_TOKEN_AUDIENCE)
+    assert isinstance(cred, service_account.IDTokenCredentials)
+    assert cred._target_audience == ID_TOKEN_AUDIENCE
+
+
+def test_fetch_id_token_credentials_no_cred_exists(monkeypatch):
+    monkeypatch.delenv(environment_vars.CREDENTIALS, raising=False)
+
+    with mock.patch(
+        "google.auth.compute_engine._metadata.ping",
+        side_effect=exceptions.TransportError(),
+    ):
+        with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+            id_token.fetch_id_token_credentials(ID_TOKEN_AUDIENCE)
+        assert excinfo.match(
+            r"Neither metadata server or valid service account credentials are found."
+        )
+
+    with mock.patch("google.auth.compute_engine._metadata.ping", return_value=False):
+        with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+            id_token.fetch_id_token_credentials(ID_TOKEN_AUDIENCE)
+        assert excinfo.match(
+            r"Neither metadata server or valid service account credentials are found."
+        )
+
+
+def test_fetch_id_token_credentials_invalid_cred_file_type(monkeypatch):
+    user_credentials_file = os.path.join(
+        os.path.dirname(__file__), "../data/authorized_user.json"
+    )
+    monkeypatch.setenv(environment_vars.CREDENTIALS, user_credentials_file)
+
+    with mock.patch("google.auth.compute_engine._metadata.ping", return_value=False):
+        with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+            id_token.fetch_id_token_credentials(ID_TOKEN_AUDIENCE)
+        assert excinfo.match(
+            r"Neither metadata server or valid service account credentials are found."
+        )
+
+
+def test_fetch_id_token_credentials_invalid_json(monkeypatch):
+    not_json_file = os.path.join(os.path.dirname(__file__), "../data/public_cert.pem")
+    monkeypatch.setenv(environment_vars.CREDENTIALS, not_json_file)
+
+    with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+        id_token.fetch_id_token_credentials(ID_TOKEN_AUDIENCE)
+    assert excinfo.match(
+        r"GOOGLE_APPLICATION_CREDENTIALS is not valid service account credentials."
+    )
+
+
+def test_fetch_id_token_credentials_invalid_cred_path(monkeypatch):
+    not_json_file = os.path.join(os.path.dirname(__file__), "../data/not_exists.json")
+    monkeypatch.setenv(environment_vars.CREDENTIALS, not_json_file)
+
+    with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+        id_token.fetch_id_token_credentials(ID_TOKEN_AUDIENCE)
+    assert excinfo.match(
+        r"GOOGLE_APPLICATION_CREDENTIALS path is either not found or invalid."
+    )
+
+
+def test_fetch_id_token(monkeypatch):
+    mock_cred = mock.MagicMock()
+    mock_cred.token = "token"
+
+    mock_req = mock.Mock()
+
+    with mock.patch(
+        "google.oauth2.id_token.fetch_id_token_credentials", return_value=mock_cred
+    ) as mock_fetch:
+        token = id_token.fetch_id_token(mock_req, ID_TOKEN_AUDIENCE)
+    mock_fetch.assert_called_once_with(ID_TOKEN_AUDIENCE, request=mock_req)
+    mock_cred.refresh.assert_called_once_with(mock_req)
+    assert token == "token"
diff --git a/tests/oauth2/test_reauth.py b/tests/oauth2/test_reauth.py
new file mode 100644
index 0000000..58d649d
--- /dev/null
+++ b/tests/oauth2/test_reauth.py
@@ -0,0 +1,329 @@
+# 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 copy
+
+import mock
+import pytest
+
+from google.auth import exceptions
+from google.oauth2 import reauth
+
+
+MOCK_REQUEST = mock.Mock()
+CHALLENGES_RESPONSE_TEMPLATE = {
+    "status": "CHALLENGE_REQUIRED",
+    "sessionId": "123",
+    "challenges": [
+        {
+            "status": "READY",
+            "challengeId": 1,
+            "challengeType": "PASSWORD",
+            "securityKey": {},
+        }
+    ],
+}
+CHALLENGES_RESPONSE_AUTHENTICATED = {
+    "status": "AUTHENTICATED",
+    "sessionId": "123",
+    "encodedProofOfReauthToken": "new_rapt_token",
+}
+
+
+class MockChallenge(object):
+    def __init__(self, name, locally_eligible, challenge_input):
+        self.name = name
+        self.is_locally_eligible = locally_eligible
+        self.challenge_input = challenge_input
+
+    def obtain_challenge_input(self, metadata):
+        return self.challenge_input
+
+
+def test_is_interactive():
+    with mock.patch("sys.stdin.isatty", return_value=True):
+        assert reauth.is_interactive()
+
+
+def test__get_challenges():
+    with mock.patch(
+        "google.oauth2._client._token_endpoint_request"
+    ) as mock_token_endpoint_request:
+        reauth._get_challenges(MOCK_REQUEST, ["SAML"], "token")
+        mock_token_endpoint_request.assert_called_with(
+            MOCK_REQUEST,
+            reauth._REAUTH_API + ":start",
+            {"supportedChallengeTypes": ["SAML"]},
+            access_token="token",
+            use_json=True,
+        )
+
+
+def test__get_challenges_with_scopes():
+    with mock.patch(
+        "google.oauth2._client._token_endpoint_request"
+    ) as mock_token_endpoint_request:
+        reauth._get_challenges(
+            MOCK_REQUEST, ["SAML"], "token", requested_scopes=["scope"]
+        )
+        mock_token_endpoint_request.assert_called_with(
+            MOCK_REQUEST,
+            reauth._REAUTH_API + ":start",
+            {
+                "supportedChallengeTypes": ["SAML"],
+                "oauthScopesForDomainPolicyLookup": ["scope"],
+            },
+            access_token="token",
+            use_json=True,
+        )
+
+
+def test__send_challenge_result():
+    with mock.patch(
+        "google.oauth2._client._token_endpoint_request"
+    ) as mock_token_endpoint_request:
+        reauth._send_challenge_result(
+            MOCK_REQUEST, "123", "1", {"credential": "password"}, "token"
+        )
+        mock_token_endpoint_request.assert_called_with(
+            MOCK_REQUEST,
+            reauth._REAUTH_API + "/123:continue",
+            {
+                "sessionId": "123",
+                "challengeId": "1",
+                "action": "RESPOND",
+                "proposalResponse": {"credential": "password"},
+            },
+            access_token="token",
+            use_json=True,
+        )
+
+
+def test__run_next_challenge_not_ready():
+    challenges_response = copy.deepcopy(CHALLENGES_RESPONSE_TEMPLATE)
+    challenges_response["challenges"][0]["status"] = "STATUS_UNSPECIFIED"
+    assert (
+        reauth._run_next_challenge(challenges_response, MOCK_REQUEST, "token") is None
+    )
+
+
+def test__run_next_challenge_not_supported():
+    challenges_response = copy.deepcopy(CHALLENGES_RESPONSE_TEMPLATE)
+    challenges_response["challenges"][0]["challengeType"] = "CHALLENGE_TYPE_UNSPECIFIED"
+    with pytest.raises(exceptions.ReauthFailError) as excinfo:
+        reauth._run_next_challenge(challenges_response, MOCK_REQUEST, "token")
+    assert excinfo.match(r"Unsupported challenge type CHALLENGE_TYPE_UNSPECIFIED")
+
+
+def test__run_next_challenge_not_locally_eligible():
+    mock_challenge = MockChallenge("PASSWORD", False, "challenge_input")
+    with mock.patch(
+        "google.oauth2.challenges.AVAILABLE_CHALLENGES", {"PASSWORD": mock_challenge}
+    ):
+        with pytest.raises(exceptions.ReauthFailError) as excinfo:
+            reauth._run_next_challenge(
+                CHALLENGES_RESPONSE_TEMPLATE, MOCK_REQUEST, "token"
+            )
+        assert excinfo.match(r"Challenge PASSWORD is not locally eligible")
+
+
+def test__run_next_challenge_no_challenge_input():
+    mock_challenge = MockChallenge("PASSWORD", True, None)
+    with mock.patch(
+        "google.oauth2.challenges.AVAILABLE_CHALLENGES", {"PASSWORD": mock_challenge}
+    ):
+        assert (
+            reauth._run_next_challenge(
+                CHALLENGES_RESPONSE_TEMPLATE, MOCK_REQUEST, "token"
+            )
+            is None
+        )
+
+
+def test__run_next_challenge_success():
+    mock_challenge = MockChallenge("PASSWORD", True, {"credential": "password"})
+    with mock.patch(
+        "google.oauth2.challenges.AVAILABLE_CHALLENGES", {"PASSWORD": mock_challenge}
+    ):
+        with mock.patch(
+            "google.oauth2.reauth._send_challenge_result"
+        ) as mock_send_challenge_result:
+            reauth._run_next_challenge(
+                CHALLENGES_RESPONSE_TEMPLATE, MOCK_REQUEST, "token"
+            )
+            mock_send_challenge_result.assert_called_with(
+                MOCK_REQUEST, "123", 1, {"credential": "password"}, "token"
+            )
+
+
+def test__obtain_rapt_authenticated():
+    with mock.patch(
+        "google.oauth2.reauth._get_challenges",
+        return_value=CHALLENGES_RESPONSE_AUTHENTICATED,
+    ):
+        assert reauth._obtain_rapt(MOCK_REQUEST, "token", None) == "new_rapt_token"
+
+
+def test__obtain_rapt_authenticated_after_run_next_challenge():
+    with mock.patch(
+        "google.oauth2.reauth._get_challenges",
+        return_value=CHALLENGES_RESPONSE_TEMPLATE,
+    ):
+        with mock.patch(
+            "google.oauth2.reauth._run_next_challenge",
+            side_effect=[
+                CHALLENGES_RESPONSE_TEMPLATE,
+                CHALLENGES_RESPONSE_AUTHENTICATED,
+            ],
+        ):
+            with mock.patch("google.oauth2.reauth.is_interactive", return_value=True):
+                assert (
+                    reauth._obtain_rapt(MOCK_REQUEST, "token", None) == "new_rapt_token"
+                )
+
+
+def test__obtain_rapt_unsupported_status():
+    challenges_response = copy.deepcopy(CHALLENGES_RESPONSE_TEMPLATE)
+    challenges_response["status"] = "STATUS_UNSPECIFIED"
+    with mock.patch(
+        "google.oauth2.reauth._get_challenges", return_value=challenges_response
+    ):
+        with pytest.raises(exceptions.ReauthFailError) as excinfo:
+            reauth._obtain_rapt(MOCK_REQUEST, "token", None)
+        assert excinfo.match(r"API error: STATUS_UNSPECIFIED")
+
+
+def test__obtain_rapt_not_interactive():
+    with mock.patch(
+        "google.oauth2.reauth._get_challenges",
+        return_value=CHALLENGES_RESPONSE_TEMPLATE,
+    ):
+        with mock.patch("google.oauth2.reauth.is_interactive", return_value=False):
+            with pytest.raises(exceptions.ReauthFailError) as excinfo:
+                reauth._obtain_rapt(MOCK_REQUEST, "token", None)
+            assert excinfo.match(r"not in an interactive session")
+
+
+def test__obtain_rapt_not_authenticated():
+    with mock.patch(
+        "google.oauth2.reauth._get_challenges",
+        return_value=CHALLENGES_RESPONSE_TEMPLATE,
+    ):
+        with mock.patch("google.oauth2.reauth.RUN_CHALLENGE_RETRY_LIMIT", 0):
+            with pytest.raises(exceptions.ReauthFailError) as excinfo:
+                reauth._obtain_rapt(MOCK_REQUEST, "token", None)
+            assert excinfo.match(r"Reauthentication failed")
+
+
+def test_get_rapt_token():
+    with mock.patch(
+        "google.oauth2._client.refresh_grant", return_value=("token", None, None, None)
+    ) as mock_refresh_grant:
+        with mock.patch(
+            "google.oauth2.reauth._obtain_rapt", return_value="new_rapt_token"
+        ) as mock_obtain_rapt:
+            assert (
+                reauth.get_rapt_token(
+                    MOCK_REQUEST,
+                    "client_id",
+                    "client_secret",
+                    "refresh_token",
+                    "token_uri",
+                )
+                == "new_rapt_token"
+            )
+            mock_refresh_grant.assert_called_with(
+                request=MOCK_REQUEST,
+                client_id="client_id",
+                client_secret="client_secret",
+                refresh_token="refresh_token",
+                token_uri="token_uri",
+                scopes=[reauth._REAUTH_SCOPE],
+            )
+            mock_obtain_rapt.assert_called_with(
+                MOCK_REQUEST, "token", requested_scopes=None
+            )
+
+
+def test_refresh_grant_failed():
+    with mock.patch(
+        "google.oauth2._client._token_endpoint_request_no_throw"
+    ) as mock_token_request:
+        mock_token_request.return_value = (False, {"error": "Bad request"})
+        with pytest.raises(exceptions.RefreshError) as excinfo:
+            reauth.refresh_grant(
+                MOCK_REQUEST,
+                "token_uri",
+                "refresh_token",
+                "client_id",
+                "client_secret",
+                scopes=["foo", "bar"],
+                rapt_token="rapt_token",
+                enable_reauth_refresh=True,
+            )
+        assert excinfo.match(r"Bad request")
+        mock_token_request.assert_called_with(
+            MOCK_REQUEST,
+            "token_uri",
+            {
+                "grant_type": "refresh_token",
+                "client_id": "client_id",
+                "client_secret": "client_secret",
+                "refresh_token": "refresh_token",
+                "scope": "foo bar",
+                "rapt": "rapt_token",
+            },
+        )
+
+
+def test_refresh_grant_success():
+    with mock.patch(
+        "google.oauth2._client._token_endpoint_request_no_throw"
+    ) as mock_token_request:
+        mock_token_request.side_effect = [
+            (False, {"error": "invalid_grant", "error_subtype": "rapt_required"}),
+            (True, {"access_token": "access_token"}),
+        ]
+        with mock.patch(
+            "google.oauth2.reauth.get_rapt_token", return_value="new_rapt_token"
+        ):
+            assert reauth.refresh_grant(
+                MOCK_REQUEST,
+                "token_uri",
+                "refresh_token",
+                "client_id",
+                "client_secret",
+                enable_reauth_refresh=True,
+            ) == (
+                "access_token",
+                "refresh_token",
+                None,
+                {"access_token": "access_token"},
+                "new_rapt_token",
+            )
+
+
+def test_refresh_grant_reauth_refresh_disabled():
+    with mock.patch(
+        "google.oauth2._client._token_endpoint_request_no_throw"
+    ) as mock_token_request:
+        mock_token_request.side_effect = [
+            (False, {"error": "invalid_grant", "error_subtype": "rapt_required"}),
+            (True, {"access_token": "access_token"}),
+        ]
+        with pytest.raises(exceptions.RefreshError) as excinfo:
+            reauth.refresh_grant(
+                MOCK_REQUEST, "token_uri", "refresh_token", "client_id", "client_secret"
+            )
+        assert excinfo.match(r"Reauthentication is needed")
diff --git a/tests/oauth2/test_service_account.py b/tests/oauth2/test_service_account.py
new file mode 100644
index 0000000..531fc4c
--- /dev/null
+++ b/tests/oauth2/test_service_account.py
@@ -0,0 +1,527 @@
+# Copyright 2016 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 datetime
+import json
+import os
+
+import mock
+
+from google.auth import _helpers
+from google.auth import crypt
+from google.auth import jwt
+from google.auth import transport
+from google.oauth2 import service_account
+
+
+DATA_DIR = os.path.join(os.path.dirname(__file__), "..", "data")
+
+with open(os.path.join(DATA_DIR, "privatekey.pem"), "rb") as fh:
+    PRIVATE_KEY_BYTES = fh.read()
+
+with open(os.path.join(DATA_DIR, "public_cert.pem"), "rb") as fh:
+    PUBLIC_CERT_BYTES = fh.read()
+
+with open(os.path.join(DATA_DIR, "other_cert.pem"), "rb") as fh:
+    OTHER_CERT_BYTES = fh.read()
+
+SERVICE_ACCOUNT_JSON_FILE = os.path.join(DATA_DIR, "service_account.json")
+
+with open(SERVICE_ACCOUNT_JSON_FILE, "r") as fh:
+    SERVICE_ACCOUNT_INFO = json.load(fh)
+
+SIGNER = crypt.RSASigner.from_string(PRIVATE_KEY_BYTES, "1")
+
+
+class TestCredentials(object):
+    SERVICE_ACCOUNT_EMAIL = "[email protected]"
+    TOKEN_URI = "https://example.com/oauth2/token"
+
+    @classmethod
+    def make_credentials(cls):
+        return service_account.Credentials(
+            SIGNER, cls.SERVICE_ACCOUNT_EMAIL, cls.TOKEN_URI
+        )
+
+    def test_from_service_account_info(self):
+        credentials = service_account.Credentials.from_service_account_info(
+            SERVICE_ACCOUNT_INFO
+        )
+
+        assert credentials._signer.key_id == SERVICE_ACCOUNT_INFO["private_key_id"]
+        assert credentials.service_account_email == SERVICE_ACCOUNT_INFO["client_email"]
+        assert credentials._token_uri == SERVICE_ACCOUNT_INFO["token_uri"]
+
+    def test_from_service_account_info_args(self):
+        info = SERVICE_ACCOUNT_INFO.copy()
+        scopes = ["email", "profile"]
+        subject = "subject"
+        additional_claims = {"meta": "data"}
+
+        credentials = service_account.Credentials.from_service_account_info(
+            info, scopes=scopes, subject=subject, additional_claims=additional_claims
+        )
+
+        assert credentials.service_account_email == info["client_email"]
+        assert credentials.project_id == info["project_id"]
+        assert credentials._signer.key_id == info["private_key_id"]
+        assert credentials._token_uri == info["token_uri"]
+        assert credentials._scopes == scopes
+        assert credentials._subject == subject
+        assert credentials._additional_claims == additional_claims
+
+    def test_from_service_account_file(self):
+        info = SERVICE_ACCOUNT_INFO.copy()
+
+        credentials = service_account.Credentials.from_service_account_file(
+            SERVICE_ACCOUNT_JSON_FILE
+        )
+
+        assert credentials.service_account_email == info["client_email"]
+        assert credentials.project_id == info["project_id"]
+        assert credentials._signer.key_id == info["private_key_id"]
+        assert credentials._token_uri == info["token_uri"]
+
+    def test_from_service_account_file_args(self):
+        info = SERVICE_ACCOUNT_INFO.copy()
+        scopes = ["email", "profile"]
+        subject = "subject"
+        additional_claims = {"meta": "data"}
+
+        credentials = service_account.Credentials.from_service_account_file(
+            SERVICE_ACCOUNT_JSON_FILE,
+            subject=subject,
+            scopes=scopes,
+            additional_claims=additional_claims,
+        )
+
+        assert credentials.service_account_email == info["client_email"]
+        assert credentials.project_id == info["project_id"]
+        assert credentials._signer.key_id == info["private_key_id"]
+        assert credentials._token_uri == info["token_uri"]
+        assert credentials._scopes == scopes
+        assert credentials._subject == subject
+        assert credentials._additional_claims == additional_claims
+
+    def test_default_state(self):
+        credentials = self.make_credentials()
+        assert not credentials.valid
+        # Expiration hasn't been set yet
+        assert not credentials.expired
+        # Scopes haven't been specified yet
+        assert credentials.requires_scopes
+
+    def test_sign_bytes(self):
+        credentials = self.make_credentials()
+        to_sign = b"123"
+        signature = credentials.sign_bytes(to_sign)
+        assert crypt.verify_signature(to_sign, signature, PUBLIC_CERT_BYTES)
+
+    def test_signer(self):
+        credentials = self.make_credentials()
+        assert isinstance(credentials.signer, crypt.Signer)
+
+    def test_signer_email(self):
+        credentials = self.make_credentials()
+        assert credentials.signer_email == self.SERVICE_ACCOUNT_EMAIL
+
+    def test_create_scoped(self):
+        credentials = self.make_credentials()
+        scopes = ["email", "profile"]
+        credentials = credentials.with_scopes(scopes)
+        assert credentials._scopes == scopes
+
+    def test_with_claims(self):
+        credentials = self.make_credentials()
+        new_credentials = credentials.with_claims({"meep": "moop"})
+        assert new_credentials._additional_claims == {"meep": "moop"}
+
+    def test_with_quota_project(self):
+        credentials = self.make_credentials()
+        new_credentials = credentials.with_quota_project("new-project-456")
+        assert new_credentials.quota_project_id == "new-project-456"
+        hdrs = {}
+        new_credentials.apply(hdrs, token="tok")
+        assert "x-goog-user-project" in hdrs
+
+    def test__with_always_use_jwt_access(self):
+        credentials = self.make_credentials()
+        assert not credentials._always_use_jwt_access
+
+        new_credentials = credentials.with_always_use_jwt_access(True)
+        assert new_credentials._always_use_jwt_access
+
+    def test__make_authorization_grant_assertion(self):
+        credentials = self.make_credentials()
+        token = credentials._make_authorization_grant_assertion()
+        payload = jwt.decode(token, PUBLIC_CERT_BYTES)
+        assert payload["iss"] == self.SERVICE_ACCOUNT_EMAIL
+        assert payload["aud"] == service_account._GOOGLE_OAUTH2_TOKEN_ENDPOINT
+
+    def test__make_authorization_grant_assertion_scoped(self):
+        credentials = self.make_credentials()
+        scopes = ["email", "profile"]
+        credentials = credentials.with_scopes(scopes)
+        token = credentials._make_authorization_grant_assertion()
+        payload = jwt.decode(token, PUBLIC_CERT_BYTES)
+        assert payload["scope"] == "email profile"
+
+    def test__make_authorization_grant_assertion_subject(self):
+        credentials = self.make_credentials()
+        subject = "[email protected]"
+        credentials = credentials.with_subject(subject)
+        token = credentials._make_authorization_grant_assertion()
+        payload = jwt.decode(token, PUBLIC_CERT_BYTES)
+        assert payload["sub"] == subject
+
+    def test_apply_with_quota_project_id(self):
+        credentials = service_account.Credentials(
+            SIGNER,
+            self.SERVICE_ACCOUNT_EMAIL,
+            self.TOKEN_URI,
+            quota_project_id="quota-project-123",
+        )
+
+        headers = {}
+        credentials.apply(headers, token="token")
+
+        assert headers["x-goog-user-project"] == "quota-project-123"
+        assert "token" in headers["authorization"]
+
+    def test_apply_with_no_quota_project_id(self):
+        credentials = service_account.Credentials(
+            SIGNER, self.SERVICE_ACCOUNT_EMAIL, self.TOKEN_URI
+        )
+
+        headers = {}
+        credentials.apply(headers, token="token")
+
+        assert "x-goog-user-project" not in headers
+        assert "token" in headers["authorization"]
+
+    @mock.patch("google.auth.jwt.Credentials", instance=True, autospec=True)
+    def test__create_self_signed_jwt(self, jwt):
+        credentials = service_account.Credentials(
+            SIGNER, self.SERVICE_ACCOUNT_EMAIL, self.TOKEN_URI
+        )
+
+        audience = "https://pubsub.googleapis.com"
+        credentials._create_self_signed_jwt(audience)
+        jwt.from_signing_credentials.assert_called_once_with(credentials, audience)
+
+    @mock.patch("google.auth.jwt.Credentials", instance=True, autospec=True)
+    def test__create_self_signed_jwt_with_user_scopes(self, jwt):
+        credentials = service_account.Credentials(
+            SIGNER, self.SERVICE_ACCOUNT_EMAIL, self.TOKEN_URI, scopes=["foo"]
+        )
+
+        audience = "https://pubsub.googleapis.com"
+        credentials._create_self_signed_jwt(audience)
+
+        # JWT should not be created if there are user-defined scopes
+        jwt.from_signing_credentials.assert_not_called()
+
+    @mock.patch("google.auth.jwt.Credentials", instance=True, autospec=True)
+    def test__create_self_signed_jwt_always_use_jwt_access_with_audience(self, jwt):
+        credentials = service_account.Credentials(
+            SIGNER,
+            self.SERVICE_ACCOUNT_EMAIL,
+            self.TOKEN_URI,
+            default_scopes=["bar", "foo"],
+            always_use_jwt_access=True,
+        )
+
+        audience = "https://pubsub.googleapis.com"
+        credentials._create_self_signed_jwt(audience)
+        jwt.from_signing_credentials.assert_called_once_with(credentials, audience)
+
+    @mock.patch("google.auth.jwt.Credentials", instance=True, autospec=True)
+    def test__create_self_signed_jwt_always_use_jwt_access_with_scopes(self, jwt):
+        credentials = service_account.Credentials(
+            SIGNER,
+            self.SERVICE_ACCOUNT_EMAIL,
+            self.TOKEN_URI,
+            scopes=["bar", "foo"],
+            always_use_jwt_access=True,
+        )
+
+        audience = "https://pubsub.googleapis.com"
+        credentials._create_self_signed_jwt(audience)
+        jwt.from_signing_credentials.assert_called_once_with(
+            credentials, None, additional_claims={"scope": "bar foo"}
+        )
+
+    @mock.patch("google.auth.jwt.Credentials", instance=True, autospec=True)
+    def test__create_self_signed_jwt_always_use_jwt_access_with_default_scopes(
+        self, jwt
+    ):
+        credentials = service_account.Credentials(
+            SIGNER,
+            self.SERVICE_ACCOUNT_EMAIL,
+            self.TOKEN_URI,
+            default_scopes=["bar", "foo"],
+            always_use_jwt_access=True,
+        )
+
+        credentials._create_self_signed_jwt(None)
+        jwt.from_signing_credentials.assert_called_once_with(
+            credentials, None, additional_claims={"scope": "bar foo"}
+        )
+
+    @mock.patch("google.auth.jwt.Credentials", instance=True, autospec=True)
+    def test__create_self_signed_jwt_always_use_jwt_access(self, jwt):
+        credentials = service_account.Credentials(
+            SIGNER,
+            self.SERVICE_ACCOUNT_EMAIL,
+            self.TOKEN_URI,
+            always_use_jwt_access=True,
+        )
+
+        credentials._create_self_signed_jwt(None)
+        jwt.from_signing_credentials.assert_not_called()
+
+    @mock.patch("google.oauth2._client.jwt_grant", autospec=True)
+    def test_refresh_success(self, jwt_grant):
+        credentials = self.make_credentials()
+        token = "token"
+        jwt_grant.return_value = (
+            token,
+            _helpers.utcnow() + datetime.timedelta(seconds=500),
+            {},
+        )
+        request = mock.create_autospec(transport.Request, instance=True)
+
+        # Refresh credentials
+        credentials.refresh(request)
+
+        # Check jwt grant call.
+        assert jwt_grant.called
+
+        called_request, token_uri, assertion = jwt_grant.call_args[0]
+        assert called_request == request
+        assert token_uri == credentials._token_uri
+        assert jwt.decode(assertion, PUBLIC_CERT_BYTES)
+        # No further assertion done on the token, as there are separate tests
+        # for checking the authorization grant assertion.
+
+        # Check that the credentials have the token.
+        assert credentials.token == token
+
+        # Check that the credentials are valid (have a token and are not
+        # expired)
+        assert credentials.valid
+
+    @mock.patch("google.oauth2._client.jwt_grant", autospec=True)
+    def test_before_request_refreshes(self, jwt_grant):
+        credentials = self.make_credentials()
+        token = "token"
+        jwt_grant.return_value = (
+            token,
+            _helpers.utcnow() + datetime.timedelta(seconds=500),
+            None,
+        )
+        request = mock.create_autospec(transport.Request, instance=True)
+
+        # Credentials should start as invalid
+        assert not credentials.valid
+
+        # before_request should cause a refresh
+        credentials.before_request(request, "GET", "http://example.com?a=1#3", {})
+
+        # The refresh endpoint should've been called.
+        assert jwt_grant.called
+
+        # Credentials should now be valid.
+        assert credentials.valid
+
+    @mock.patch("google.auth.jwt.Credentials._make_jwt")
+    def test_refresh_with_jwt_credentials(self, make_jwt):
+        credentials = self.make_credentials()
+        credentials._create_self_signed_jwt("https://pubsub.googleapis.com")
+
+        request = mock.create_autospec(transport.Request, instance=True)
+
+        token = "token"
+        expiry = _helpers.utcnow() + datetime.timedelta(seconds=500)
+        make_jwt.return_value = (token, expiry)
+
+        # Credentials should start as invalid
+        assert not credentials.valid
+
+        # before_request should cause a refresh
+        credentials.before_request(request, "GET", "http://example.com?a=1#3", {})
+
+        # Credentials should now be valid.
+        assert credentials.valid
+
+        # Assert make_jwt was called
+        assert make_jwt.called_once()
+
+        assert credentials.token == token
+        assert credentials.expiry == expiry
+
+    @mock.patch("google.oauth2._client.jwt_grant", autospec=True)
+    @mock.patch("google.auth.jwt.Credentials.refresh", autospec=True)
+    def test_refresh_jwt_not_used_for_domain_wide_delegation(
+        self, self_signed_jwt_refresh, jwt_grant
+    ):
+        # Create a domain wide delegation credentials by setting the subject.
+        credentials = service_account.Credentials(
+            SIGNER,
+            self.SERVICE_ACCOUNT_EMAIL,
+            self.TOKEN_URI,
+            always_use_jwt_access=True,
+            subject="subject",
+        )
+        credentials._create_self_signed_jwt("https://pubsub.googleapis.com")
+        jwt_grant.return_value = (
+            "token",
+            _helpers.utcnow() + datetime.timedelta(seconds=500),
+            {},
+        )
+        request = mock.create_autospec(transport.Request, instance=True)
+
+        # Refresh credentials
+        credentials.refresh(request)
+
+        # Make sure we are using jwt_grant and not self signed JWT refresh
+        # method to obtain the token.
+        assert jwt_grant.called
+        assert not self_signed_jwt_refresh.called
+
+
+class TestIDTokenCredentials(object):
+    SERVICE_ACCOUNT_EMAIL = "[email protected]"
+    TOKEN_URI = "https://example.com/oauth2/token"
+    TARGET_AUDIENCE = "https://example.com"
+
+    @classmethod
+    def make_credentials(cls):
+        return service_account.IDTokenCredentials(
+            SIGNER, cls.SERVICE_ACCOUNT_EMAIL, cls.TOKEN_URI, cls.TARGET_AUDIENCE
+        )
+
+    def test_from_service_account_info(self):
+        credentials = service_account.IDTokenCredentials.from_service_account_info(
+            SERVICE_ACCOUNT_INFO, target_audience=self.TARGET_AUDIENCE
+        )
+
+        assert credentials._signer.key_id == SERVICE_ACCOUNT_INFO["private_key_id"]
+        assert credentials.service_account_email == SERVICE_ACCOUNT_INFO["client_email"]
+        assert credentials._token_uri == SERVICE_ACCOUNT_INFO["token_uri"]
+        assert credentials._target_audience == self.TARGET_AUDIENCE
+
+    def test_from_service_account_file(self):
+        info = SERVICE_ACCOUNT_INFO.copy()
+
+        credentials = service_account.IDTokenCredentials.from_service_account_file(
+            SERVICE_ACCOUNT_JSON_FILE, target_audience=self.TARGET_AUDIENCE
+        )
+
+        assert credentials.service_account_email == info["client_email"]
+        assert credentials._signer.key_id == info["private_key_id"]
+        assert credentials._token_uri == info["token_uri"]
+        assert credentials._target_audience == self.TARGET_AUDIENCE
+
+    def test_default_state(self):
+        credentials = self.make_credentials()
+        assert not credentials.valid
+        # Expiration hasn't been set yet
+        assert not credentials.expired
+
+    def test_sign_bytes(self):
+        credentials = self.make_credentials()
+        to_sign = b"123"
+        signature = credentials.sign_bytes(to_sign)
+        assert crypt.verify_signature(to_sign, signature, PUBLIC_CERT_BYTES)
+
+    def test_signer(self):
+        credentials = self.make_credentials()
+        assert isinstance(credentials.signer, crypt.Signer)
+
+    def test_signer_email(self):
+        credentials = self.make_credentials()
+        assert credentials.signer_email == self.SERVICE_ACCOUNT_EMAIL
+
+    def test_with_target_audience(self):
+        credentials = self.make_credentials()
+        new_credentials = credentials.with_target_audience("https://new.example.com")
+        assert new_credentials._target_audience == "https://new.example.com"
+
+    def test_with_quota_project(self):
+        credentials = self.make_credentials()
+        new_credentials = credentials.with_quota_project("project-foo")
+        assert new_credentials._quota_project_id == "project-foo"
+
+    def test__make_authorization_grant_assertion(self):
+        credentials = self.make_credentials()
+        token = credentials._make_authorization_grant_assertion()
+        payload = jwt.decode(token, PUBLIC_CERT_BYTES)
+        assert payload["iss"] == self.SERVICE_ACCOUNT_EMAIL
+        assert payload["aud"] == service_account._GOOGLE_OAUTH2_TOKEN_ENDPOINT
+        assert payload["target_audience"] == self.TARGET_AUDIENCE
+
+    @mock.patch("google.oauth2._client.id_token_jwt_grant", autospec=True)
+    def test_refresh_success(self, id_token_jwt_grant):
+        credentials = self.make_credentials()
+        token = "token"
+        id_token_jwt_grant.return_value = (
+            token,
+            _helpers.utcnow() + datetime.timedelta(seconds=500),
+            {},
+        )
+        request = mock.create_autospec(transport.Request, instance=True)
+
+        # Refresh credentials
+        credentials.refresh(request)
+
+        # Check jwt grant call.
+        assert id_token_jwt_grant.called
+
+        called_request, token_uri, assertion = id_token_jwt_grant.call_args[0]
+        assert called_request == request
+        assert token_uri == credentials._token_uri
+        assert jwt.decode(assertion, PUBLIC_CERT_BYTES)
+        # No further assertion done on the token, as there are separate tests
+        # for checking the authorization grant assertion.
+
+        # Check that the credentials have the token.
+        assert credentials.token == token
+
+        # Check that the credentials are valid (have a token and are not
+        # expired)
+        assert credentials.valid
+
+    @mock.patch("google.oauth2._client.id_token_jwt_grant", autospec=True)
+    def test_before_request_refreshes(self, id_token_jwt_grant):
+        credentials = self.make_credentials()
+        token = "token"
+        id_token_jwt_grant.return_value = (
+            token,
+            _helpers.utcnow() + datetime.timedelta(seconds=500),
+            None,
+        )
+        request = mock.create_autospec(transport.Request, instance=True)
+
+        # Credentials should start as invalid
+        assert not credentials.valid
+
+        # before_request should cause a refresh
+        credentials.before_request(request, "GET", "http://example.com?a=1#3", {})
+
+        # The refresh endpoint should've been called.
+        assert id_token_jwt_grant.called
+
+        # Credentials should now be valid.
+        assert credentials.valid
diff --git a/tests/oauth2/test_sts.py b/tests/oauth2/test_sts.py
new file mode 100644
index 0000000..e8e008d
--- /dev/null
+++ b/tests/oauth2/test_sts.py
@@ -0,0 +1,395 @@
+# 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.
+
+import json
+
+import mock
+import pytest
+from six.moves import http_client
+from six.moves import urllib
+
+from google.auth import exceptions
+from google.auth import transport
+from google.oauth2 import sts
+from google.oauth2 import utils
+
+CLIENT_ID = "username"
+CLIENT_SECRET = "password"
+# Base64 encoding of "username:password"
+BASIC_AUTH_ENCODING = "dXNlcm5hbWU6cGFzc3dvcmQ="
+
+
+class TestStsClient(object):
+    GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange"
+    RESOURCE = "https://api.example.com/"
+    AUDIENCE = "urn:example:cooperation-context"
+    SCOPES = ["scope1", "scope2"]
+    REQUESTED_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token"
+    SUBJECT_TOKEN = "HEADER.SUBJECT_TOKEN_PAYLOAD.SIGNATURE"
+    SUBJECT_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:jwt"
+    ACTOR_TOKEN = "HEADER.ACTOR_TOKEN_PAYLOAD.SIGNATURE"
+    ACTOR_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:jwt"
+    TOKEN_EXCHANGE_ENDPOINT = "https://example.com/token.oauth2"
+    ADDON_HEADERS = {"x-client-version": "0.1.2"}
+    ADDON_OPTIONS = {"additional": {"non-standard": ["options"], "other": "some-value"}}
+    SUCCESS_RESPONSE = {
+        "access_token": "ACCESS_TOKEN",
+        "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
+        "token_type": "Bearer",
+        "expires_in": 3600,
+        "scope": "scope1 scope2",
+    }
+    ERROR_RESPONSE = {
+        "error": "invalid_request",
+        "error_description": "Invalid subject token",
+        "error_uri": "https://tools.ietf.org/html/rfc6749",
+    }
+    CLIENT_AUTH_BASIC = utils.ClientAuthentication(
+        utils.ClientAuthType.basic, CLIENT_ID, CLIENT_SECRET
+    )
+    CLIENT_AUTH_REQUEST_BODY = utils.ClientAuthentication(
+        utils.ClientAuthType.request_body, CLIENT_ID, CLIENT_SECRET
+    )
+
+    @classmethod
+    def make_client(cls, client_auth=None):
+        return sts.Client(cls.TOKEN_EXCHANGE_ENDPOINT, client_auth)
+
+    @classmethod
+    def make_mock_request(cls, data, status=http_client.OK):
+        response = mock.create_autospec(transport.Response, instance=True)
+        response.status = status
+        response.data = json.dumps(data).encode("utf-8")
+
+        request = mock.create_autospec(transport.Request)
+        request.return_value = response
+
+        return request
+
+    @classmethod
+    def assert_request_kwargs(cls, request_kwargs, headers, request_data):
+        """Asserts the request was called with the expected parameters.
+        """
+        assert request_kwargs["url"] == cls.TOKEN_EXCHANGE_ENDPOINT
+        assert request_kwargs["method"] == "POST"
+        assert request_kwargs["headers"] == headers
+        assert request_kwargs["body"] is not None
+        body_tuples = urllib.parse.parse_qsl(request_kwargs["body"])
+        for (k, v) in body_tuples:
+            assert v.decode("utf-8") == request_data[k.decode("utf-8")]
+        assert len(body_tuples) == len(request_data.keys())
+
+    def test_exchange_token_full_success_without_auth(self):
+        """Test token exchange success without client authentication using full
+        parameters.
+        """
+        client = self.make_client()
+        headers = self.ADDON_HEADERS.copy()
+        headers["Content-Type"] = "application/x-www-form-urlencoded"
+        request_data = {
+            "grant_type": self.GRANT_TYPE,
+            "resource": self.RESOURCE,
+            "audience": self.AUDIENCE,
+            "scope": " ".join(self.SCOPES),
+            "requested_token_type": self.REQUESTED_TOKEN_TYPE,
+            "subject_token": self.SUBJECT_TOKEN,
+            "subject_token_type": self.SUBJECT_TOKEN_TYPE,
+            "actor_token": self.ACTOR_TOKEN,
+            "actor_token_type": self.ACTOR_TOKEN_TYPE,
+            "options": urllib.parse.quote(json.dumps(self.ADDON_OPTIONS)),
+        }
+        request = self.make_mock_request(
+            status=http_client.OK, data=self.SUCCESS_RESPONSE
+        )
+
+        response = client.exchange_token(
+            request,
+            self.GRANT_TYPE,
+            self.SUBJECT_TOKEN,
+            self.SUBJECT_TOKEN_TYPE,
+            self.RESOURCE,
+            self.AUDIENCE,
+            self.SCOPES,
+            self.REQUESTED_TOKEN_TYPE,
+            self.ACTOR_TOKEN,
+            self.ACTOR_TOKEN_TYPE,
+            self.ADDON_OPTIONS,
+            self.ADDON_HEADERS,
+        )
+
+        self.assert_request_kwargs(request.call_args[1], headers, request_data)
+        assert response == self.SUCCESS_RESPONSE
+
+    def test_exchange_token_partial_success_without_auth(self):
+        """Test token exchange success without client authentication using
+        partial (required only) parameters.
+        """
+        client = self.make_client()
+        headers = {"Content-Type": "application/x-www-form-urlencoded"}
+        request_data = {
+            "grant_type": self.GRANT_TYPE,
+            "audience": self.AUDIENCE,
+            "requested_token_type": self.REQUESTED_TOKEN_TYPE,
+            "subject_token": self.SUBJECT_TOKEN,
+            "subject_token_type": self.SUBJECT_TOKEN_TYPE,
+        }
+        request = self.make_mock_request(
+            status=http_client.OK, data=self.SUCCESS_RESPONSE
+        )
+
+        response = client.exchange_token(
+            request,
+            grant_type=self.GRANT_TYPE,
+            subject_token=self.SUBJECT_TOKEN,
+            subject_token_type=self.SUBJECT_TOKEN_TYPE,
+            audience=self.AUDIENCE,
+            requested_token_type=self.REQUESTED_TOKEN_TYPE,
+        )
+
+        self.assert_request_kwargs(request.call_args[1], headers, request_data)
+        assert response == self.SUCCESS_RESPONSE
+
+    def test_exchange_token_non200_without_auth(self):
+        """Test token exchange without client auth responding with non-200 status.
+        """
+        client = self.make_client()
+        request = self.make_mock_request(
+            status=http_client.BAD_REQUEST, data=self.ERROR_RESPONSE
+        )
+
+        with pytest.raises(exceptions.OAuthError) as excinfo:
+            client.exchange_token(
+                request,
+                self.GRANT_TYPE,
+                self.SUBJECT_TOKEN,
+                self.SUBJECT_TOKEN_TYPE,
+                self.RESOURCE,
+                self.AUDIENCE,
+                self.SCOPES,
+                self.REQUESTED_TOKEN_TYPE,
+                self.ACTOR_TOKEN,
+                self.ACTOR_TOKEN_TYPE,
+                self.ADDON_OPTIONS,
+                self.ADDON_HEADERS,
+            )
+
+        assert excinfo.match(
+            r"Error code invalid_request: Invalid subject token - https://tools.ietf.org/html/rfc6749"
+        )
+
+    def test_exchange_token_full_success_with_basic_auth(self):
+        """Test token exchange success with basic client authentication using full
+        parameters.
+        """
+        client = self.make_client(self.CLIENT_AUTH_BASIC)
+        headers = self.ADDON_HEADERS.copy()
+        headers["Content-Type"] = "application/x-www-form-urlencoded"
+        headers["Authorization"] = "Basic {}".format(BASIC_AUTH_ENCODING)
+        request_data = {
+            "grant_type": self.GRANT_TYPE,
+            "resource": self.RESOURCE,
+            "audience": self.AUDIENCE,
+            "scope": " ".join(self.SCOPES),
+            "requested_token_type": self.REQUESTED_TOKEN_TYPE,
+            "subject_token": self.SUBJECT_TOKEN,
+            "subject_token_type": self.SUBJECT_TOKEN_TYPE,
+            "actor_token": self.ACTOR_TOKEN,
+            "actor_token_type": self.ACTOR_TOKEN_TYPE,
+            "options": urllib.parse.quote(json.dumps(self.ADDON_OPTIONS)),
+        }
+        request = self.make_mock_request(
+            status=http_client.OK, data=self.SUCCESS_RESPONSE
+        )
+
+        response = client.exchange_token(
+            request,
+            self.GRANT_TYPE,
+            self.SUBJECT_TOKEN,
+            self.SUBJECT_TOKEN_TYPE,
+            self.RESOURCE,
+            self.AUDIENCE,
+            self.SCOPES,
+            self.REQUESTED_TOKEN_TYPE,
+            self.ACTOR_TOKEN,
+            self.ACTOR_TOKEN_TYPE,
+            self.ADDON_OPTIONS,
+            self.ADDON_HEADERS,
+        )
+
+        self.assert_request_kwargs(request.call_args[1], headers, request_data)
+        assert response == self.SUCCESS_RESPONSE
+
+    def test_exchange_token_partial_success_with_basic_auth(self):
+        """Test token exchange success with basic client authentication using
+        partial (required only) parameters.
+        """
+        client = self.make_client(self.CLIENT_AUTH_BASIC)
+        headers = {
+            "Content-Type": "application/x-www-form-urlencoded",
+            "Authorization": "Basic {}".format(BASIC_AUTH_ENCODING),
+        }
+        request_data = {
+            "grant_type": self.GRANT_TYPE,
+            "audience": self.AUDIENCE,
+            "requested_token_type": self.REQUESTED_TOKEN_TYPE,
+            "subject_token": self.SUBJECT_TOKEN,
+            "subject_token_type": self.SUBJECT_TOKEN_TYPE,
+        }
+        request = self.make_mock_request(
+            status=http_client.OK, data=self.SUCCESS_RESPONSE
+        )
+
+        response = client.exchange_token(
+            request,
+            grant_type=self.GRANT_TYPE,
+            subject_token=self.SUBJECT_TOKEN,
+            subject_token_type=self.SUBJECT_TOKEN_TYPE,
+            audience=self.AUDIENCE,
+            requested_token_type=self.REQUESTED_TOKEN_TYPE,
+        )
+
+        self.assert_request_kwargs(request.call_args[1], headers, request_data)
+        assert response == self.SUCCESS_RESPONSE
+
+    def test_exchange_token_non200_with_basic_auth(self):
+        """Test token exchange with basic client auth responding with non-200
+        status.
+        """
+        client = self.make_client(self.CLIENT_AUTH_BASIC)
+        request = self.make_mock_request(
+            status=http_client.BAD_REQUEST, data=self.ERROR_RESPONSE
+        )
+
+        with pytest.raises(exceptions.OAuthError) as excinfo:
+            client.exchange_token(
+                request,
+                self.GRANT_TYPE,
+                self.SUBJECT_TOKEN,
+                self.SUBJECT_TOKEN_TYPE,
+                self.RESOURCE,
+                self.AUDIENCE,
+                self.SCOPES,
+                self.REQUESTED_TOKEN_TYPE,
+                self.ACTOR_TOKEN,
+                self.ACTOR_TOKEN_TYPE,
+                self.ADDON_OPTIONS,
+                self.ADDON_HEADERS,
+            )
+
+        assert excinfo.match(
+            r"Error code invalid_request: Invalid subject token - https://tools.ietf.org/html/rfc6749"
+        )
+
+    def test_exchange_token_full_success_with_reqbody_auth(self):
+        """Test token exchange success with request body client authenticaiton
+        using full parameters.
+        """
+        client = self.make_client(self.CLIENT_AUTH_REQUEST_BODY)
+        headers = self.ADDON_HEADERS.copy()
+        headers["Content-Type"] = "application/x-www-form-urlencoded"
+        request_data = {
+            "grant_type": self.GRANT_TYPE,
+            "resource": self.RESOURCE,
+            "audience": self.AUDIENCE,
+            "scope": " ".join(self.SCOPES),
+            "requested_token_type": self.REQUESTED_TOKEN_TYPE,
+            "subject_token": self.SUBJECT_TOKEN,
+            "subject_token_type": self.SUBJECT_TOKEN_TYPE,
+            "actor_token": self.ACTOR_TOKEN,
+            "actor_token_type": self.ACTOR_TOKEN_TYPE,
+            "options": urllib.parse.quote(json.dumps(self.ADDON_OPTIONS)),
+            "client_id": CLIENT_ID,
+            "client_secret": CLIENT_SECRET,
+        }
+        request = self.make_mock_request(
+            status=http_client.OK, data=self.SUCCESS_RESPONSE
+        )
+
+        response = client.exchange_token(
+            request,
+            self.GRANT_TYPE,
+            self.SUBJECT_TOKEN,
+            self.SUBJECT_TOKEN_TYPE,
+            self.RESOURCE,
+            self.AUDIENCE,
+            self.SCOPES,
+            self.REQUESTED_TOKEN_TYPE,
+            self.ACTOR_TOKEN,
+            self.ACTOR_TOKEN_TYPE,
+            self.ADDON_OPTIONS,
+            self.ADDON_HEADERS,
+        )
+
+        self.assert_request_kwargs(request.call_args[1], headers, request_data)
+        assert response == self.SUCCESS_RESPONSE
+
+    def test_exchange_token_partial_success_with_reqbody_auth(self):
+        """Test token exchange success with request body client authentication
+        using partial (required only) parameters.
+        """
+        client = self.make_client(self.CLIENT_AUTH_REQUEST_BODY)
+        headers = {"Content-Type": "application/x-www-form-urlencoded"}
+        request_data = {
+            "grant_type": self.GRANT_TYPE,
+            "audience": self.AUDIENCE,
+            "requested_token_type": self.REQUESTED_TOKEN_TYPE,
+            "subject_token": self.SUBJECT_TOKEN,
+            "subject_token_type": self.SUBJECT_TOKEN_TYPE,
+            "client_id": CLIENT_ID,
+            "client_secret": CLIENT_SECRET,
+        }
+        request = self.make_mock_request(
+            status=http_client.OK, data=self.SUCCESS_RESPONSE
+        )
+
+        response = client.exchange_token(
+            request,
+            grant_type=self.GRANT_TYPE,
+            subject_token=self.SUBJECT_TOKEN,
+            subject_token_type=self.SUBJECT_TOKEN_TYPE,
+            audience=self.AUDIENCE,
+            requested_token_type=self.REQUESTED_TOKEN_TYPE,
+        )
+
+        self.assert_request_kwargs(request.call_args[1], headers, request_data)
+        assert response == self.SUCCESS_RESPONSE
+
+    def test_exchange_token_non200_with_reqbody_auth(self):
+        """Test token exchange with POST request body client auth responding
+        with non-200 status.
+        """
+        client = self.make_client(self.CLIENT_AUTH_REQUEST_BODY)
+        request = self.make_mock_request(
+            status=http_client.BAD_REQUEST, data=self.ERROR_RESPONSE
+        )
+
+        with pytest.raises(exceptions.OAuthError) as excinfo:
+            client.exchange_token(
+                request,
+                self.GRANT_TYPE,
+                self.SUBJECT_TOKEN,
+                self.SUBJECT_TOKEN_TYPE,
+                self.RESOURCE,
+                self.AUDIENCE,
+                self.SCOPES,
+                self.REQUESTED_TOKEN_TYPE,
+                self.ACTOR_TOKEN,
+                self.ACTOR_TOKEN_TYPE,
+                self.ADDON_OPTIONS,
+                self.ADDON_HEADERS,
+            )
+
+        assert excinfo.match(
+            r"Error code invalid_request: Invalid subject token - https://tools.ietf.org/html/rfc6749"
+        )
diff --git a/tests/oauth2/test_utils.py b/tests/oauth2/test_utils.py
new file mode 100644
index 0000000..6de9ff5
--- /dev/null
+++ b/tests/oauth2/test_utils.py
@@ -0,0 +1,264 @@
+# 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.
+
+import json
+
+import pytest
+
+from google.auth import exceptions
+from google.oauth2 import utils
+
+
+CLIENT_ID = "username"
+CLIENT_SECRET = "password"
+# Base64 encoding of "username:password"
+BASIC_AUTH_ENCODING = "dXNlcm5hbWU6cGFzc3dvcmQ="
+# Base64 encoding of "username:"
+BASIC_AUTH_ENCODING_SECRETLESS = "dXNlcm5hbWU6"
+
+
+class AuthHandler(utils.OAuthClientAuthHandler):
+    def __init__(self, client_auth=None):
+        super(AuthHandler, self).__init__(client_auth)
+
+    def apply_client_authentication_options(
+        self, headers, request_body=None, bearer_token=None
+    ):
+        return super(AuthHandler, self).apply_client_authentication_options(
+            headers, request_body, bearer_token
+        )
+
+
+class TestClientAuthentication(object):
+    @classmethod
+    def make_client_auth(cls, client_secret=None):
+        return utils.ClientAuthentication(
+            utils.ClientAuthType.basic, CLIENT_ID, client_secret
+        )
+
+    def test_initialization_with_client_secret(self):
+        client_auth = self.make_client_auth(CLIENT_SECRET)
+
+        assert client_auth.client_auth_type == utils.ClientAuthType.basic
+        assert client_auth.client_id == CLIENT_ID
+        assert client_auth.client_secret == CLIENT_SECRET
+
+    def test_initialization_no_client_secret(self):
+        client_auth = self.make_client_auth()
+
+        assert client_auth.client_auth_type == utils.ClientAuthType.basic
+        assert client_auth.client_id == CLIENT_ID
+        assert client_auth.client_secret is None
+
+
+class TestOAuthClientAuthHandler(object):
+    CLIENT_AUTH_BASIC = utils.ClientAuthentication(
+        utils.ClientAuthType.basic, CLIENT_ID, CLIENT_SECRET
+    )
+    CLIENT_AUTH_BASIC_SECRETLESS = utils.ClientAuthentication(
+        utils.ClientAuthType.basic, CLIENT_ID
+    )
+    CLIENT_AUTH_REQUEST_BODY = utils.ClientAuthentication(
+        utils.ClientAuthType.request_body, CLIENT_ID, CLIENT_SECRET
+    )
+    CLIENT_AUTH_REQUEST_BODY_SECRETLESS = utils.ClientAuthentication(
+        utils.ClientAuthType.request_body, CLIENT_ID
+    )
+
+    @classmethod
+    def make_oauth_client_auth_handler(cls, client_auth=None):
+        return AuthHandler(client_auth)
+
+    def test_apply_client_authentication_options_none(self):
+        headers = {"Content-Type": "application/json"}
+        request_body = {"foo": "bar"}
+        auth_handler = self.make_oauth_client_auth_handler()
+
+        auth_handler.apply_client_authentication_options(headers, request_body)
+
+        assert headers == {"Content-Type": "application/json"}
+        assert request_body == {"foo": "bar"}
+
+    def test_apply_client_authentication_options_basic(self):
+        headers = {"Content-Type": "application/json"}
+        request_body = {"foo": "bar"}
+        auth_handler = self.make_oauth_client_auth_handler(self.CLIENT_AUTH_BASIC)
+
+        auth_handler.apply_client_authentication_options(headers, request_body)
+
+        assert headers == {
+            "Content-Type": "application/json",
+            "Authorization": "Basic {}".format(BASIC_AUTH_ENCODING),
+        }
+        assert request_body == {"foo": "bar"}
+
+    def test_apply_client_authentication_options_basic_nosecret(self):
+        headers = {"Content-Type": "application/json"}
+        request_body = {"foo": "bar"}
+        auth_handler = self.make_oauth_client_auth_handler(
+            self.CLIENT_AUTH_BASIC_SECRETLESS
+        )
+
+        auth_handler.apply_client_authentication_options(headers, request_body)
+
+        assert headers == {
+            "Content-Type": "application/json",
+            "Authorization": "Basic {}".format(BASIC_AUTH_ENCODING_SECRETLESS),
+        }
+        assert request_body == {"foo": "bar"}
+
+    def test_apply_client_authentication_options_request_body(self):
+        headers = {"Content-Type": "application/json"}
+        request_body = {"foo": "bar"}
+        auth_handler = self.make_oauth_client_auth_handler(
+            self.CLIENT_AUTH_REQUEST_BODY
+        )
+
+        auth_handler.apply_client_authentication_options(headers, request_body)
+
+        assert headers == {"Content-Type": "application/json"}
+        assert request_body == {
+            "foo": "bar",
+            "client_id": CLIENT_ID,
+            "client_secret": CLIENT_SECRET,
+        }
+
+    def test_apply_client_authentication_options_request_body_nosecret(self):
+        headers = {"Content-Type": "application/json"}
+        request_body = {"foo": "bar"}
+        auth_handler = self.make_oauth_client_auth_handler(
+            self.CLIENT_AUTH_REQUEST_BODY_SECRETLESS
+        )
+
+        auth_handler.apply_client_authentication_options(headers, request_body)
+
+        assert headers == {"Content-Type": "application/json"}
+        assert request_body == {
+            "foo": "bar",
+            "client_id": CLIENT_ID,
+            "client_secret": "",
+        }
+
+    def test_apply_client_authentication_options_request_body_no_body(self):
+        headers = {"Content-Type": "application/json"}
+        auth_handler = self.make_oauth_client_auth_handler(
+            self.CLIENT_AUTH_REQUEST_BODY
+        )
+
+        with pytest.raises(exceptions.OAuthError) as excinfo:
+            auth_handler.apply_client_authentication_options(headers)
+
+        assert excinfo.match(r"HTTP request does not support request-body")
+
+    def test_apply_client_authentication_options_bearer_token(self):
+        bearer_token = "ACCESS_TOKEN"
+        headers = {"Content-Type": "application/json"}
+        request_body = {"foo": "bar"}
+        auth_handler = self.make_oauth_client_auth_handler()
+
+        auth_handler.apply_client_authentication_options(
+            headers, request_body, bearer_token
+        )
+
+        assert headers == {
+            "Content-Type": "application/json",
+            "Authorization": "Bearer {}".format(bearer_token),
+        }
+        assert request_body == {"foo": "bar"}
+
+    def test_apply_client_authentication_options_bearer_and_basic(self):
+        bearer_token = "ACCESS_TOKEN"
+        headers = {"Content-Type": "application/json"}
+        request_body = {"foo": "bar"}
+        auth_handler = self.make_oauth_client_auth_handler(self.CLIENT_AUTH_BASIC)
+
+        auth_handler.apply_client_authentication_options(
+            headers, request_body, bearer_token
+        )
+
+        # Bearer token should have higher priority.
+        assert headers == {
+            "Content-Type": "application/json",
+            "Authorization": "Bearer {}".format(bearer_token),
+        }
+        assert request_body == {"foo": "bar"}
+
+    def test_apply_client_authentication_options_bearer_and_request_body(self):
+        bearer_token = "ACCESS_TOKEN"
+        headers = {"Content-Type": "application/json"}
+        request_body = {"foo": "bar"}
+        auth_handler = self.make_oauth_client_auth_handler(
+            self.CLIENT_AUTH_REQUEST_BODY
+        )
+
+        auth_handler.apply_client_authentication_options(
+            headers, request_body, bearer_token
+        )
+
+        # Bearer token should have higher priority.
+        assert headers == {
+            "Content-Type": "application/json",
+            "Authorization": "Bearer {}".format(bearer_token),
+        }
+        assert request_body == {"foo": "bar"}
+
+
+def test__handle_error_response_code_only():
+    error_resp = {"error": "unsupported_grant_type"}
+    response_data = json.dumps(error_resp)
+
+    with pytest.raises(exceptions.OAuthError) as excinfo:
+        utils.handle_error_response(response_data)
+
+    assert excinfo.match(r"Error code unsupported_grant_type")
+
+
+def test__handle_error_response_code_description():
+    error_resp = {
+        "error": "unsupported_grant_type",
+        "error_description": "The provided grant_type is unsupported",
+    }
+    response_data = json.dumps(error_resp)
+
+    with pytest.raises(exceptions.OAuthError) as excinfo:
+        utils.handle_error_response(response_data)
+
+    assert excinfo.match(
+        r"Error code unsupported_grant_type: The provided grant_type is unsupported"
+    )
+
+
+def test__handle_error_response_code_description_uri():
+    error_resp = {
+        "error": "unsupported_grant_type",
+        "error_description": "The provided grant_type is unsupported",
+        "error_uri": "https://tools.ietf.org/html/rfc6749",
+    }
+    response_data = json.dumps(error_resp)
+
+    with pytest.raises(exceptions.OAuthError) as excinfo:
+        utils.handle_error_response(response_data)
+
+    assert excinfo.match(
+        r"Error code unsupported_grant_type: The provided grant_type is unsupported - https://tools.ietf.org/html/rfc6749"
+    )
+
+
+def test__handle_error_response_non_json():
+    response_data = "Oops, something wrong happened"
+
+    with pytest.raises(exceptions.OAuthError) as excinfo:
+        utils.handle_error_response(response_data)
+
+    assert excinfo.match(r"Oops, something wrong happened")
diff --git a/tests/test__cloud_sdk.py b/tests/test__cloud_sdk.py
new file mode 100644
index 0000000..31cb6c2
--- /dev/null
+++ b/tests/test__cloud_sdk.py
@@ -0,0 +1,188 @@
+# Copyright 2016 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 io
+import json
+import os
+import subprocess
+
+import mock
+import pytest
+
+from google.auth import _cloud_sdk
+from google.auth import environment_vars
+from google.auth import exceptions
+
+
+DATA_DIR = os.path.join(os.path.dirname(__file__), "data")
+AUTHORIZED_USER_FILE = os.path.join(DATA_DIR, "authorized_user.json")
+
+with io.open(AUTHORIZED_USER_FILE) as fh:
+    AUTHORIZED_USER_FILE_DATA = json.load(fh)
+
+SERVICE_ACCOUNT_FILE = os.path.join(DATA_DIR, "service_account.json")
+
+with io.open(SERVICE_ACCOUNT_FILE) as fh:
+    SERVICE_ACCOUNT_FILE_DATA = json.load(fh)
+
+with io.open(os.path.join(DATA_DIR, "cloud_sdk_config.json"), "rb") as fh:
+    CLOUD_SDK_CONFIG_FILE_DATA = fh.read()
+
+
[email protected](
+    "data, expected_project_id",
+    [
+        (CLOUD_SDK_CONFIG_FILE_DATA, "example-project"),
+        (b"I am some bad json", None),
+        (b"{}", None),
+    ],
+)
+def test_get_project_id(data, expected_project_id):
+    check_output_patch = mock.patch(
+        "subprocess.check_output", autospec=True, return_value=data
+    )
+
+    with check_output_patch as check_output:
+        project_id = _cloud_sdk.get_project_id()
+
+    assert project_id == expected_project_id
+    assert check_output.called
+
+
[email protected](
+    "subprocess.check_output",
+    autospec=True,
+    side_effect=subprocess.CalledProcessError(-1, None),
+)
+def test_get_project_id_call_error(check_output):
+    project_id = _cloud_sdk.get_project_id()
+    assert project_id is None
+    assert check_output.called
+
+
+def test__run_subprocess_ignore_stderr():
+    command = [
+        "python",
+        "-c",
+        "from __future__ import print_function;"
+        + "import sys;"
+        + "print('error', file=sys.stderr);"
+        + "print('output', file=sys.stdout)",
+    ]
+
+    # If we ignore stderr, then the output only has stdout
+    output = _cloud_sdk._run_subprocess_ignore_stderr(command)
+    assert output == b"output\n"
+
+    # If we pipe stderr to stdout, then the output is mixed with stdout and stderr.
+    output = subprocess.check_output(command, stderr=subprocess.STDOUT)
+    assert output == b"output\nerror\n" or output == b"error\noutput\n"
+
+
[email protected]("os.name", new="nt")
+def test_get_project_id_windows():
+    check_output_patch = mock.patch(
+        "subprocess.check_output",
+        autospec=True,
+        return_value=CLOUD_SDK_CONFIG_FILE_DATA,
+    )
+
+    with check_output_patch as check_output:
+        project_id = _cloud_sdk.get_project_id()
+
+    assert project_id == "example-project"
+    assert check_output.called
+    # Make sure the executable is `gcloud.cmd`.
+    args = check_output.call_args[0]
+    command = args[0]
+    executable = command[0]
+    assert executable == "gcloud.cmd"
+
+
[email protected]("google.auth._cloud_sdk.get_config_path", autospec=True)
+def test_get_application_default_credentials_path(get_config_dir):
+    config_path = "config_path"
+    get_config_dir.return_value = config_path
+    credentials_path = _cloud_sdk.get_application_default_credentials_path()
+    assert credentials_path == os.path.join(
+        config_path, _cloud_sdk._CREDENTIALS_FILENAME
+    )
+
+
+def test_get_config_path_env_var(monkeypatch):
+    config_path_sentinel = "config_path"
+    monkeypatch.setenv(environment_vars.CLOUD_SDK_CONFIG_DIR, config_path_sentinel)
+    config_path = _cloud_sdk.get_config_path()
+    assert config_path == config_path_sentinel
+
+
[email protected]("os.path.expanduser")
+def test_get_config_path_unix(expanduser):
+    expanduser.side_effect = lambda path: path
+
+    config_path = _cloud_sdk.get_config_path()
+
+    assert os.path.split(config_path) == ("~/.config", _cloud_sdk._CONFIG_DIRECTORY)
+
+
[email protected]("os.name", new="nt")
+def test_get_config_path_windows(monkeypatch):
+    appdata = "appdata"
+    monkeypatch.setenv(_cloud_sdk._WINDOWS_CONFIG_ROOT_ENV_VAR, appdata)
+
+    config_path = _cloud_sdk.get_config_path()
+
+    assert os.path.split(config_path) == (appdata, _cloud_sdk._CONFIG_DIRECTORY)
+
+
[email protected]("os.name", new="nt")
+def test_get_config_path_no_appdata(monkeypatch):
+    monkeypatch.delenv(_cloud_sdk._WINDOWS_CONFIG_ROOT_ENV_VAR, raising=False)
+    monkeypatch.setenv("SystemDrive", "G:")
+
+    config_path = _cloud_sdk.get_config_path()
+
+    assert os.path.split(config_path) == ("G:/\\", _cloud_sdk._CONFIG_DIRECTORY)
+
+
[email protected]("os.name", new="nt")
[email protected]("subprocess.check_output", autospec=True)
+def test_get_auth_access_token_windows(check_output):
+    check_output.return_value = b"access_token\n"
+
+    token = _cloud_sdk.get_auth_access_token()
+    assert token == "access_token"
+    check_output.assert_called_with(
+        ("gcloud.cmd", "auth", "print-access-token"), stderr=subprocess.STDOUT
+    )
+
+
[email protected]("subprocess.check_output", autospec=True)
+def test_get_auth_access_token_with_account(check_output):
+    check_output.return_value = b"access_token\n"
+
+    token = _cloud_sdk.get_auth_access_token(account="account")
+    assert token == "access_token"
+    check_output.assert_called_with(
+        ("gcloud", "auth", "print-access-token", "--account=account"),
+        stderr=subprocess.STDOUT,
+    )
+
+
[email protected]("subprocess.check_output", autospec=True)
+def test_get_auth_access_token_with_exception(check_output):
+    check_output.side_effect = OSError()
+
+    with pytest.raises(exceptions.UserAccessTokenError):
+        _cloud_sdk.get_auth_access_token(account="account")
diff --git a/tests/test__default.py b/tests/test__default.py
new file mode 100644
index 0000000..1ce03cf
--- /dev/null
+++ b/tests/test__default.py
@@ -0,0 +1,996 @@
+# Copyright 2016 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 json
+import os
+
+import mock
+import pytest
+
+from google.auth import _default
+from google.auth import app_engine
+from google.auth import aws
+from google.auth import compute_engine
+from google.auth import credentials
+from google.auth import environment_vars
+from google.auth import exceptions
+from google.auth import external_account
+from google.auth import identity_pool
+from google.oauth2 import service_account
+import google.oauth2.credentials
+
+
+DATA_DIR = os.path.join(os.path.dirname(__file__), "data")
+AUTHORIZED_USER_FILE = os.path.join(DATA_DIR, "authorized_user.json")
+
+with open(AUTHORIZED_USER_FILE) as fh:
+    AUTHORIZED_USER_FILE_DATA = json.load(fh)
+
+AUTHORIZED_USER_CLOUD_SDK_FILE = os.path.join(
+    DATA_DIR, "authorized_user_cloud_sdk.json"
+)
+
+AUTHORIZED_USER_CLOUD_SDK_WITH_QUOTA_PROJECT_ID_FILE = os.path.join(
+    DATA_DIR, "authorized_user_cloud_sdk_with_quota_project_id.json"
+)
+
+SERVICE_ACCOUNT_FILE = os.path.join(DATA_DIR, "service_account.json")
+
+CLIENT_SECRETS_FILE = os.path.join(DATA_DIR, "client_secrets.json")
+
+with open(SERVICE_ACCOUNT_FILE) as fh:
+    SERVICE_ACCOUNT_FILE_DATA = json.load(fh)
+
+SUBJECT_TOKEN_TEXT_FILE = os.path.join(DATA_DIR, "external_subject_token.txt")
+TOKEN_URL = "https://sts.googleapis.com/v1/token"
+AUDIENCE = "//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID"
+WORKFORCE_AUDIENCE = (
+    "//iam.googleapis.com/locations/global/workforcePools/POOL_ID/providers/PROVIDER_ID"
+)
+WORKFORCE_POOL_USER_PROJECT = "WORKFORCE_POOL_USER_PROJECT_NUMBER"
+REGION_URL = "http://169.254.169.254/latest/meta-data/placement/availability-zone"
+SECURITY_CREDS_URL = "http://169.254.169.254/latest/meta-data/iam/security-credentials"
+CRED_VERIFICATION_URL = (
+    "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15"
+)
+IDENTITY_POOL_DATA = {
+    "type": "external_account",
+    "audience": AUDIENCE,
+    "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
+    "token_url": TOKEN_URL,
+    "credential_source": {"file": SUBJECT_TOKEN_TEXT_FILE},
+}
+AWS_DATA = {
+    "type": "external_account",
+    "audience": AUDIENCE,
+    "subject_token_type": "urn:ietf:params:aws:token-type:aws4_request",
+    "token_url": TOKEN_URL,
+    "credential_source": {
+        "environment_id": "aws1",
+        "region_url": REGION_URL,
+        "url": SECURITY_CREDS_URL,
+        "regional_cred_verification_url": CRED_VERIFICATION_URL,
+    },
+}
+SERVICE_ACCOUNT_EMAIL = "[email protected]"
+SERVICE_ACCOUNT_IMPERSONATION_URL = (
+    "https://us-east1-iamcredentials.googleapis.com/v1/projects/-"
+    + "/serviceAccounts/{}:generateAccessToken".format(SERVICE_ACCOUNT_EMAIL)
+)
+IMPERSONATED_IDENTITY_POOL_DATA = {
+    "type": "external_account",
+    "audience": AUDIENCE,
+    "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
+    "token_url": TOKEN_URL,
+    "credential_source": {"file": SUBJECT_TOKEN_TEXT_FILE},
+    "service_account_impersonation_url": SERVICE_ACCOUNT_IMPERSONATION_URL,
+}
+IMPERSONATED_AWS_DATA = {
+    "type": "external_account",
+    "audience": AUDIENCE,
+    "subject_token_type": "urn:ietf:params:aws:token-type:aws4_request",
+    "token_url": TOKEN_URL,
+    "credential_source": {
+        "environment_id": "aws1",
+        "region_url": REGION_URL,
+        "url": SECURITY_CREDS_URL,
+        "regional_cred_verification_url": CRED_VERIFICATION_URL,
+    },
+    "service_account_impersonation_url": SERVICE_ACCOUNT_IMPERSONATION_URL,
+}
+IDENTITY_POOL_WORKFORCE_DATA = {
+    "type": "external_account",
+    "audience": WORKFORCE_AUDIENCE,
+    "subject_token_type": "urn:ietf:params:oauth:token-type:id_token",
+    "token_url": TOKEN_URL,
+    "credential_source": {"file": SUBJECT_TOKEN_TEXT_FILE},
+    "workforce_pool_user_project": WORKFORCE_POOL_USER_PROJECT,
+}
+IMPERSONATED_IDENTITY_POOL_WORKFORCE_DATA = {
+    "type": "external_account",
+    "audience": WORKFORCE_AUDIENCE,
+    "subject_token_type": "urn:ietf:params:oauth:token-type:id_token",
+    "token_url": TOKEN_URL,
+    "credential_source": {"file": SUBJECT_TOKEN_TEXT_FILE},
+    "service_account_impersonation_url": SERVICE_ACCOUNT_IMPERSONATION_URL,
+    "workforce_pool_user_project": WORKFORCE_POOL_USER_PROJECT,
+}
+
+MOCK_CREDENTIALS = mock.Mock(spec=credentials.CredentialsWithQuotaProject)
+MOCK_CREDENTIALS.with_quota_project.return_value = MOCK_CREDENTIALS
+
+
+def get_project_id_side_effect(self, request=None):
+    # If no scopes are set, this will always return None.
+    if not self.scopes:
+        return None
+    return mock.sentinel.project_id
+
+
+LOAD_FILE_PATCH = mock.patch(
+    "google.auth._default.load_credentials_from_file",
+    return_value=(MOCK_CREDENTIALS, mock.sentinel.project_id),
+    autospec=True,
+)
+EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH = mock.patch.object(
+    external_account.Credentials,
+    "get_project_id",
+    side_effect=get_project_id_side_effect,
+    autospec=True,
+)
+
+
+def test_load_credentials_from_missing_file():
+    with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+        _default.load_credentials_from_file("")
+
+    assert excinfo.match(r"not found")
+
+
+def test_load_credentials_from_file_invalid_json(tmpdir):
+    jsonfile = tmpdir.join("invalid.json")
+    jsonfile.write("{")
+
+    with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+        _default.load_credentials_from_file(str(jsonfile))
+
+    assert excinfo.match(r"not a valid json file")
+
+
+def test_load_credentials_from_file_invalid_type(tmpdir):
+    jsonfile = tmpdir.join("invalid.json")
+    jsonfile.write(json.dumps({"type": "not-a-real-type"}))
+
+    with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+        _default.load_credentials_from_file(str(jsonfile))
+
+    assert excinfo.match(r"does not have a valid type")
+
+
+def test_load_credentials_from_file_authorized_user():
+    credentials, project_id = _default.load_credentials_from_file(AUTHORIZED_USER_FILE)
+    assert isinstance(credentials, google.oauth2.credentials.Credentials)
+    assert project_id is None
+
+
+def test_load_credentials_from_file_no_type(tmpdir):
+    # use the client_secrets.json, which is valid json but not a
+    # loadable credentials type
+    with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+        _default.load_credentials_from_file(CLIENT_SECRETS_FILE)
+
+    assert excinfo.match(r"does not have a valid type")
+    assert excinfo.match(r"Type is None")
+
+
+def test_load_credentials_from_file_authorized_user_bad_format(tmpdir):
+    filename = tmpdir.join("authorized_user_bad.json")
+    filename.write(json.dumps({"type": "authorized_user"}))
+
+    with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+        _default.load_credentials_from_file(str(filename))
+
+    assert excinfo.match(r"Failed to load authorized user")
+    assert excinfo.match(r"missing fields")
+
+
+def test_load_credentials_from_file_authorized_user_cloud_sdk():
+    with pytest.warns(UserWarning, match="Cloud SDK"):
+        credentials, project_id = _default.load_credentials_from_file(
+            AUTHORIZED_USER_CLOUD_SDK_FILE
+        )
+    assert isinstance(credentials, google.oauth2.credentials.Credentials)
+    assert project_id is None
+
+    # No warning if the json file has quota project id.
+    credentials, project_id = _default.load_credentials_from_file(
+        AUTHORIZED_USER_CLOUD_SDK_WITH_QUOTA_PROJECT_ID_FILE
+    )
+    assert isinstance(credentials, google.oauth2.credentials.Credentials)
+    assert project_id is None
+
+
+def test_load_credentials_from_file_authorized_user_cloud_sdk_with_scopes():
+    with pytest.warns(UserWarning, match="Cloud SDK"):
+        credentials, project_id = _default.load_credentials_from_file(
+            AUTHORIZED_USER_CLOUD_SDK_FILE,
+            scopes=["https://www.google.com/calendar/feeds"],
+        )
+    assert isinstance(credentials, google.oauth2.credentials.Credentials)
+    assert project_id is None
+    assert credentials.scopes == ["https://www.google.com/calendar/feeds"]
+
+
+def test_load_credentials_from_file_authorized_user_cloud_sdk_with_quota_project():
+    credentials, project_id = _default.load_credentials_from_file(
+        AUTHORIZED_USER_CLOUD_SDK_FILE, quota_project_id="project-foo"
+    )
+
+    assert isinstance(credentials, google.oauth2.credentials.Credentials)
+    assert project_id is None
+    assert credentials.quota_project_id == "project-foo"
+
+
+def test_load_credentials_from_file_service_account():
+    credentials, project_id = _default.load_credentials_from_file(SERVICE_ACCOUNT_FILE)
+    assert isinstance(credentials, service_account.Credentials)
+    assert project_id == SERVICE_ACCOUNT_FILE_DATA["project_id"]
+
+
+def test_load_credentials_from_file_service_account_with_scopes():
+    credentials, project_id = _default.load_credentials_from_file(
+        SERVICE_ACCOUNT_FILE, scopes=["https://www.google.com/calendar/feeds"]
+    )
+    assert isinstance(credentials, service_account.Credentials)
+    assert project_id == SERVICE_ACCOUNT_FILE_DATA["project_id"]
+    assert credentials.scopes == ["https://www.google.com/calendar/feeds"]
+
+
+def test_load_credentials_from_file_service_account_with_quota_project():
+    credentials, project_id = _default.load_credentials_from_file(
+        SERVICE_ACCOUNT_FILE, quota_project_id="project-foo"
+    )
+    assert isinstance(credentials, service_account.Credentials)
+    assert project_id == SERVICE_ACCOUNT_FILE_DATA["project_id"]
+    assert credentials.quota_project_id == "project-foo"
+
+
+def test_load_credentials_from_file_service_account_bad_format(tmpdir):
+    filename = tmpdir.join("serivce_account_bad.json")
+    filename.write(json.dumps({"type": "service_account"}))
+
+    with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+        _default.load_credentials_from_file(str(filename))
+
+    assert excinfo.match(r"Failed to load service account")
+    assert excinfo.match(r"missing fields")
+
+
+@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
+def test_load_credentials_from_file_external_account_identity_pool(
+    get_project_id, tmpdir
+):
+    config_file = tmpdir.join("config.json")
+    config_file.write(json.dumps(IDENTITY_POOL_DATA))
+    credentials, project_id = _default.load_credentials_from_file(str(config_file))
+
+    assert isinstance(credentials, identity_pool.Credentials)
+    # Since no scopes are specified, the project ID cannot be determined.
+    assert project_id is None
+    assert get_project_id.called
+
+
+@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
+def test_load_credentials_from_file_external_account_aws(get_project_id, tmpdir):
+    config_file = tmpdir.join("config.json")
+    config_file.write(json.dumps(AWS_DATA))
+    credentials, project_id = _default.load_credentials_from_file(str(config_file))
+
+    assert isinstance(credentials, aws.Credentials)
+    # Since no scopes are specified, the project ID cannot be determined.
+    assert project_id is None
+    assert get_project_id.called
+
+
+@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
+def test_load_credentials_from_file_external_account_identity_pool_impersonated(
+    get_project_id, tmpdir
+):
+    config_file = tmpdir.join("config.json")
+    config_file.write(json.dumps(IMPERSONATED_IDENTITY_POOL_DATA))
+    credentials, project_id = _default.load_credentials_from_file(str(config_file))
+
+    assert isinstance(credentials, identity_pool.Credentials)
+    assert not credentials.is_user
+    assert not credentials.is_workforce_pool
+    # Since no scopes are specified, the project ID cannot be determined.
+    assert project_id is None
+    assert get_project_id.called
+
+
+@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
+def test_load_credentials_from_file_external_account_aws_impersonated(
+    get_project_id, tmpdir
+):
+    config_file = tmpdir.join("config.json")
+    config_file.write(json.dumps(IMPERSONATED_AWS_DATA))
+    credentials, project_id = _default.load_credentials_from_file(str(config_file))
+
+    assert isinstance(credentials, aws.Credentials)
+    assert not credentials.is_user
+    assert not credentials.is_workforce_pool
+    # Since no scopes are specified, the project ID cannot be determined.
+    assert project_id is None
+    assert get_project_id.called
+
+
+@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
+def test_load_credentials_from_file_external_account_workforce(get_project_id, tmpdir):
+    config_file = tmpdir.join("config.json")
+    config_file.write(json.dumps(IDENTITY_POOL_WORKFORCE_DATA))
+    credentials, project_id = _default.load_credentials_from_file(str(config_file))
+
+    assert isinstance(credentials, identity_pool.Credentials)
+    assert credentials.is_user
+    assert credentials.is_workforce_pool
+    # Since no scopes are specified, the project ID cannot be determined.
+    assert project_id is None
+    assert get_project_id.called
+
+
+@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
+def test_load_credentials_from_file_external_account_workforce_impersonated(
+    get_project_id, tmpdir
+):
+    config_file = tmpdir.join("config.json")
+    config_file.write(json.dumps(IMPERSONATED_IDENTITY_POOL_WORKFORCE_DATA))
+    credentials, project_id = _default.load_credentials_from_file(str(config_file))
+
+    assert isinstance(credentials, identity_pool.Credentials)
+    assert not credentials.is_user
+    assert credentials.is_workforce_pool
+    # Since no scopes are specified, the project ID cannot be determined.
+    assert project_id is None
+    assert get_project_id.called
+
+
+@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
+def test_load_credentials_from_file_external_account_with_user_and_default_scopes(
+    get_project_id, tmpdir
+):
+    config_file = tmpdir.join("config.json")
+    config_file.write(json.dumps(IDENTITY_POOL_DATA))
+    credentials, project_id = _default.load_credentials_from_file(
+        str(config_file),
+        scopes=["https://www.google.com/calendar/feeds"],
+        default_scopes=["https://www.googleapis.com/auth/cloud-platform"],
+    )
+
+    assert isinstance(credentials, identity_pool.Credentials)
+    # Since scopes are specified, the project ID can be determined.
+    assert project_id is mock.sentinel.project_id
+    assert credentials.scopes == ["https://www.google.com/calendar/feeds"]
+    assert credentials.default_scopes == [
+        "https://www.googleapis.com/auth/cloud-platform"
+    ]
+
+
+@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
+def test_load_credentials_from_file_external_account_with_quota_project(
+    get_project_id, tmpdir
+):
+    config_file = tmpdir.join("config.json")
+    config_file.write(json.dumps(IDENTITY_POOL_DATA))
+    credentials, project_id = _default.load_credentials_from_file(
+        str(config_file), quota_project_id="project-foo"
+    )
+
+    assert isinstance(credentials, identity_pool.Credentials)
+    # Since no scopes are specified, the project ID cannot be determined.
+    assert project_id is None
+    assert credentials.quota_project_id == "project-foo"
+
+
+def test_load_credentials_from_file_external_account_bad_format(tmpdir):
+    filename = tmpdir.join("external_account_bad.json")
+    filename.write(json.dumps({"type": "external_account"}))
+
+    with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+        _default.load_credentials_from_file(str(filename))
+
+    assert excinfo.match(
+        "Failed to load external account credentials from {}".format(str(filename))
+    )
+
+
+@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
+def test_load_credentials_from_file_external_account_explicit_request(
+    get_project_id, tmpdir
+):
+    config_file = tmpdir.join("config.json")
+    config_file.write(json.dumps(IDENTITY_POOL_DATA))
+    credentials, project_id = _default.load_credentials_from_file(
+        str(config_file),
+        request=mock.sentinel.request,
+        scopes=["https://www.googleapis.com/auth/cloud-platform"],
+    )
+
+    assert isinstance(credentials, identity_pool.Credentials)
+    # Since scopes are specified, the project ID can be determined.
+    assert project_id is mock.sentinel.project_id
+    get_project_id.assert_called_with(credentials, request=mock.sentinel.request)
+
+
[email protected](os.environ, {}, clear=True)
+def test__get_explicit_environ_credentials_no_env():
+    assert _default._get_explicit_environ_credentials() == (None, None)
+
+
[email protected]("quota_project_id", [None, "project-foo"])
+@LOAD_FILE_PATCH
+def test__get_explicit_environ_credentials(load, quota_project_id, monkeypatch):
+    monkeypatch.setenv(environment_vars.CREDENTIALS, "filename")
+
+    credentials, project_id = _default._get_explicit_environ_credentials(
+        quota_project_id=quota_project_id
+    )
+
+    assert credentials is MOCK_CREDENTIALS
+    assert project_id is mock.sentinel.project_id
+    load.assert_called_with("filename", quota_project_id=quota_project_id)
+
+
+@LOAD_FILE_PATCH
+def test__get_explicit_environ_credentials_no_project_id(load, monkeypatch):
+    load.return_value = MOCK_CREDENTIALS, None
+    monkeypatch.setenv(environment_vars.CREDENTIALS, "filename")
+
+    credentials, project_id = _default._get_explicit_environ_credentials()
+
+    assert credentials is MOCK_CREDENTIALS
+    assert project_id is None
+
+
[email protected]("quota_project_id", [None, "project-foo"])
[email protected](
+    "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True
+)
[email protected]("google.auth._default._get_gcloud_sdk_credentials", autospec=True)
+def test__get_explicit_environ_credentials_fallback_to_gcloud(
+    get_gcloud_creds, get_adc_path, quota_project_id, monkeypatch
+):
+    # Set explicit credentials path to cloud sdk credentials path.
+    get_adc_path.return_value = "filename"
+    monkeypatch.setenv(environment_vars.CREDENTIALS, "filename")
+
+    _default._get_explicit_environ_credentials(quota_project_id=quota_project_id)
+
+    # Check we fall back to cloud sdk flow since explicit credentials path is
+    # cloud sdk credentials path
+    get_gcloud_creds.assert_called_with(quota_project_id=quota_project_id)
+
+
[email protected]("quota_project_id", [None, "project-foo"])
+@LOAD_FILE_PATCH
[email protected](
+    "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True
+)
+def test__get_gcloud_sdk_credentials(get_adc_path, load, quota_project_id):
+    get_adc_path.return_value = SERVICE_ACCOUNT_FILE
+
+    credentials, project_id = _default._get_gcloud_sdk_credentials(
+        quota_project_id=quota_project_id
+    )
+
+    assert credentials is MOCK_CREDENTIALS
+    assert project_id is mock.sentinel.project_id
+    load.assert_called_with(SERVICE_ACCOUNT_FILE, quota_project_id=quota_project_id)
+
+
[email protected](
+    "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True
+)
+def test__get_gcloud_sdk_credentials_non_existent(get_adc_path, tmpdir):
+    non_existent = tmpdir.join("non-existent")
+    get_adc_path.return_value = str(non_existent)
+
+    credentials, project_id = _default._get_gcloud_sdk_credentials()
+
+    assert credentials is None
+    assert project_id is None
+
+
[email protected](
+    "google.auth._cloud_sdk.get_project_id",
+    return_value=mock.sentinel.project_id,
+    autospec=True,
+)
[email protected]("os.path.isfile", return_value=True, autospec=True)
+@LOAD_FILE_PATCH
+def test__get_gcloud_sdk_credentials_project_id(load, unused_isfile, get_project_id):
+    # Don't return a project ID from load file, make the function check
+    # the Cloud SDK project.
+    load.return_value = MOCK_CREDENTIALS, None
+
+    credentials, project_id = _default._get_gcloud_sdk_credentials()
+
+    assert credentials == MOCK_CREDENTIALS
+    assert project_id == mock.sentinel.project_id
+    assert get_project_id.called
+
+
[email protected]("google.auth._cloud_sdk.get_project_id", return_value=None, autospec=True)
[email protected]("os.path.isfile", return_value=True)
+@LOAD_FILE_PATCH
+def test__get_gcloud_sdk_credentials_no_project_id(load, unused_isfile, get_project_id):
+    # Don't return a project ID from load file, make the function check
+    # the Cloud SDK project.
+    load.return_value = MOCK_CREDENTIALS, None
+
+    credentials, project_id = _default._get_gcloud_sdk_credentials()
+
+    assert credentials == MOCK_CREDENTIALS
+    assert project_id is None
+    assert get_project_id.called
+
+
+class _AppIdentityModule(object):
+    """The interface of the App Idenity app engine module.
+    See https://cloud.google.com/appengine/docs/standard/python/refdocs\
+    /google.appengine.api.app_identity.app_identity
+    """
+
+    def get_application_id(self):
+        raise NotImplementedError()
+
+
[email protected]
+def app_identity(monkeypatch):
+    """Mocks the app_identity module for google.auth.app_engine."""
+    app_identity_module = mock.create_autospec(_AppIdentityModule, instance=True)
+    monkeypatch.setattr(app_engine, "app_identity", app_identity_module)
+    yield app_identity_module
+
+
[email protected](os.environ)
+def test__get_gae_credentials_gen1(app_identity):
+    os.environ[environment_vars.LEGACY_APPENGINE_RUNTIME] = "python27"
+    app_identity.get_application_id.return_value = mock.sentinel.project
+
+    credentials, project_id = _default._get_gae_credentials()
+
+    assert isinstance(credentials, app_engine.Credentials)
+    assert project_id == mock.sentinel.project
+
+
[email protected](os.environ)
+def test__get_gae_credentials_gen2():
+    os.environ["GAE_RUNTIME"] = "python37"
+    credentials, project_id = _default._get_gae_credentials()
+    assert credentials is None
+    assert project_id is None
+
+
[email protected](os.environ)
+def test__get_gae_credentials_gen2_backwards_compat():
+    # compat helpers may copy GAE_RUNTIME to APPENGINE_RUNTIME
+    # for backwards compatibility with code that relies on it
+    os.environ[environment_vars.LEGACY_APPENGINE_RUNTIME] = "python37"
+    os.environ["GAE_RUNTIME"] = "python37"
+    credentials, project_id = _default._get_gae_credentials()
+    assert credentials is None
+    assert project_id is None
+
+
+def test__get_gae_credentials_env_unset():
+    assert environment_vars.LEGACY_APPENGINE_RUNTIME not in os.environ
+    assert "GAE_RUNTIME" not in os.environ
+    credentials, project_id = _default._get_gae_credentials()
+    assert credentials is None
+    assert project_id is None
+
+
[email protected](os.environ)
+def test__get_gae_credentials_no_app_engine():
+    # test both with and without LEGACY_APPENGINE_RUNTIME setting
+    assert environment_vars.LEGACY_APPENGINE_RUNTIME not in os.environ
+
+    import sys
+
+    with mock.patch.dict(sys.modules, {"google.auth.app_engine": None}):
+        credentials, project_id = _default._get_gae_credentials()
+        assert credentials is None
+        assert project_id is None
+
+        os.environ[environment_vars.LEGACY_APPENGINE_RUNTIME] = "python27"
+        credentials, project_id = _default._get_gae_credentials()
+        assert credentials is None
+        assert project_id is None
+
+
[email protected](os.environ)
[email protected](app_engine, "app_identity", new=None)
+def test__get_gae_credentials_no_apis():
+    # test both with and without LEGACY_APPENGINE_RUNTIME setting
+    assert environment_vars.LEGACY_APPENGINE_RUNTIME not in os.environ
+
+    credentials, project_id = _default._get_gae_credentials()
+    assert credentials is None
+    assert project_id is None
+
+    os.environ[environment_vars.LEGACY_APPENGINE_RUNTIME] = "python27"
+    credentials, project_id = _default._get_gae_credentials()
+    assert credentials is None
+    assert project_id is None
+
+
[email protected](
+    "google.auth.compute_engine._metadata.ping", return_value=True, autospec=True
+)
[email protected](
+    "google.auth.compute_engine._metadata.get_project_id",
+    return_value="example-project",
+    autospec=True,
+)
+def test__get_gce_credentials(unused_get, unused_ping):
+    credentials, project_id = _default._get_gce_credentials()
+
+    assert isinstance(credentials, compute_engine.Credentials)
+    assert project_id == "example-project"
+
+
[email protected](
+    "google.auth.compute_engine._metadata.ping", return_value=False, autospec=True
+)
+def test__get_gce_credentials_no_ping(unused_ping):
+    credentials, project_id = _default._get_gce_credentials()
+
+    assert credentials is None
+    assert project_id is None
+
+
[email protected](
+    "google.auth.compute_engine._metadata.ping", return_value=True, autospec=True
+)
[email protected](
+    "google.auth.compute_engine._metadata.get_project_id",
+    side_effect=exceptions.TransportError(),
+    autospec=True,
+)
+def test__get_gce_credentials_no_project_id(unused_get, unused_ping):
+    credentials, project_id = _default._get_gce_credentials()
+
+    assert isinstance(credentials, compute_engine.Credentials)
+    assert project_id is None
+
+
+def test__get_gce_credentials_no_compute_engine():
+    import sys
+
+    with mock.patch.dict("sys.modules"):
+        sys.modules["google.auth.compute_engine"] = None
+        credentials, project_id = _default._get_gce_credentials()
+        assert credentials is None
+        assert project_id is None
+
+
[email protected](
+    "google.auth.compute_engine._metadata.ping", return_value=False, autospec=True
+)
+def test__get_gce_credentials_explicit_request(ping):
+    _default._get_gce_credentials(mock.sentinel.request)
+    ping.assert_called_with(request=mock.sentinel.request)
+
+
[email protected](
+    "google.auth._default._get_explicit_environ_credentials",
+    return_value=(MOCK_CREDENTIALS, mock.sentinel.project_id),
+    autospec=True,
+)
+def test_default_early_out(unused_get):
+    assert _default.default() == (MOCK_CREDENTIALS, mock.sentinel.project_id)
+
+
[email protected](
+    "google.auth._default._get_explicit_environ_credentials",
+    return_value=(MOCK_CREDENTIALS, mock.sentinel.project_id),
+    autospec=True,
+)
+def test_default_explict_project_id(unused_get, monkeypatch):
+    monkeypatch.setenv(environment_vars.PROJECT, "explicit-env")
+    assert _default.default() == (MOCK_CREDENTIALS, "explicit-env")
+
+
[email protected](
+    "google.auth._default._get_explicit_environ_credentials",
+    return_value=(MOCK_CREDENTIALS, mock.sentinel.project_id),
+    autospec=True,
+)
+def test_default_explict_legacy_project_id(unused_get, monkeypatch):
+    monkeypatch.setenv(environment_vars.LEGACY_PROJECT, "explicit-env")
+    assert _default.default() == (MOCK_CREDENTIALS, "explicit-env")
+
+
[email protected]("logging.Logger.warning", autospec=True)
[email protected](
+    "google.auth._default._get_explicit_environ_credentials",
+    return_value=(MOCK_CREDENTIALS, None),
+    autospec=True,
+)
[email protected](
+    "google.auth._default._get_gcloud_sdk_credentials",
+    return_value=(MOCK_CREDENTIALS, None),
+    autospec=True,
+)
[email protected](
+    "google.auth._default._get_gae_credentials",
+    return_value=(MOCK_CREDENTIALS, None),
+    autospec=True,
+)
[email protected](
+    "google.auth._default._get_gce_credentials",
+    return_value=(MOCK_CREDENTIALS, None),
+    autospec=True,
+)
+def test_default_without_project_id(
+    unused_gce, unused_gae, unused_sdk, unused_explicit, logger_warning
+):
+    assert _default.default() == (MOCK_CREDENTIALS, None)
+    logger_warning.assert_called_with(mock.ANY, mock.ANY, mock.ANY)
+
+
[email protected](
+    "google.auth._default._get_explicit_environ_credentials",
+    return_value=(None, None),
+    autospec=True,
+)
[email protected](
+    "google.auth._default._get_gcloud_sdk_credentials",
+    return_value=(None, None),
+    autospec=True,
+)
[email protected](
+    "google.auth._default._get_gae_credentials",
+    return_value=(None, None),
+    autospec=True,
+)
[email protected](
+    "google.auth._default._get_gce_credentials",
+    return_value=(None, None),
+    autospec=True,
+)
+def test_default_fail(unused_gce, unused_gae, unused_sdk, unused_explicit):
+    with pytest.raises(exceptions.DefaultCredentialsError):
+        assert _default.default()
+
+
[email protected](
+    "google.auth._default._get_explicit_environ_credentials",
+    return_value=(MOCK_CREDENTIALS, mock.sentinel.project_id),
+    autospec=True,
+)
[email protected](
+    "google.auth.credentials.with_scopes_if_required",
+    return_value=MOCK_CREDENTIALS,
+    autospec=True,
+)
+def test_default_scoped(with_scopes, unused_get):
+    scopes = ["one", "two"]
+
+    credentials, project_id = _default.default(scopes=scopes)
+
+    assert credentials == with_scopes.return_value
+    assert project_id == mock.sentinel.project_id
+    with_scopes.assert_called_once_with(MOCK_CREDENTIALS, scopes, default_scopes=None)
+
+
[email protected](
+    "google.auth._default._get_explicit_environ_credentials",
+    return_value=(MOCK_CREDENTIALS, mock.sentinel.project_id),
+    autospec=True,
+)
+def test_default_quota_project(with_quota_project):
+    credentials, project_id = _default.default(quota_project_id="project-foo")
+
+    MOCK_CREDENTIALS.with_quota_project.assert_called_once_with("project-foo")
+    assert project_id == mock.sentinel.project_id
+
+
[email protected](
+    "google.auth._default._get_explicit_environ_credentials",
+    return_value=(MOCK_CREDENTIALS, mock.sentinel.project_id),
+    autospec=True,
+)
+def test_default_no_app_engine_compute_engine_module(unused_get):
+    """
+    google.auth.compute_engine and google.auth.app_engine are both optional
+    to allow not including them when using this package. This verifies
+    that default fails gracefully if these modules are absent
+    """
+    import sys
+
+    with mock.patch.dict("sys.modules"):
+        sys.modules["google.auth.compute_engine"] = None
+        sys.modules["google.auth.app_engine"] = None
+        assert _default.default() == (MOCK_CREDENTIALS, mock.sentinel.project_id)
+
+
+@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
+def test_default_environ_external_credentials_identity_pool(
+    get_project_id, monkeypatch, tmpdir
+):
+    config_file = tmpdir.join("config.json")
+    config_file.write(json.dumps(IDENTITY_POOL_DATA))
+    monkeypatch.setenv(environment_vars.CREDENTIALS, str(config_file))
+
+    credentials, project_id = _default.default()
+
+    assert isinstance(credentials, identity_pool.Credentials)
+    assert not credentials.is_user
+    assert not credentials.is_workforce_pool
+    # Without scopes, project ID cannot be determined.
+    assert project_id is None
+
+
+@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
+def test_default_environ_external_credentials_identity_pool_impersonated(
+    get_project_id, monkeypatch, tmpdir
+):
+    config_file = tmpdir.join("config.json")
+    config_file.write(json.dumps(IMPERSONATED_IDENTITY_POOL_DATA))
+    monkeypatch.setenv(environment_vars.CREDENTIALS, str(config_file))
+
+    credentials, project_id = _default.default(
+        scopes=["https://www.google.com/calendar/feeds"]
+    )
+
+    assert isinstance(credentials, identity_pool.Credentials)
+    assert not credentials.is_user
+    assert not credentials.is_workforce_pool
+    assert project_id is mock.sentinel.project_id
+    assert credentials.scopes == ["https://www.google.com/calendar/feeds"]
+
+
+@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
+def test_default_environ_external_credentials_aws_impersonated(
+    get_project_id, monkeypatch, tmpdir
+):
+    config_file = tmpdir.join("config.json")
+    config_file.write(json.dumps(IMPERSONATED_AWS_DATA))
+    monkeypatch.setenv(environment_vars.CREDENTIALS, str(config_file))
+
+    credentials, project_id = _default.default(
+        scopes=["https://www.google.com/calendar/feeds"]
+    )
+
+    assert isinstance(credentials, aws.Credentials)
+    assert not credentials.is_user
+    assert not credentials.is_workforce_pool
+    assert project_id is mock.sentinel.project_id
+    assert credentials.scopes == ["https://www.google.com/calendar/feeds"]
+
+
+@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
+def test_default_environ_external_credentials_workforce(
+    get_project_id, monkeypatch, tmpdir
+):
+    config_file = tmpdir.join("config.json")
+    config_file.write(json.dumps(IDENTITY_POOL_WORKFORCE_DATA))
+    monkeypatch.setenv(environment_vars.CREDENTIALS, str(config_file))
+
+    credentials, project_id = _default.default(
+        scopes=["https://www.google.com/calendar/feeds"]
+    )
+
+    assert isinstance(credentials, identity_pool.Credentials)
+    assert credentials.is_user
+    assert credentials.is_workforce_pool
+    assert project_id is mock.sentinel.project_id
+    assert credentials.scopes == ["https://www.google.com/calendar/feeds"]
+
+
+@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
+def test_default_environ_external_credentials_workforce_impersonated(
+    get_project_id, monkeypatch, tmpdir
+):
+    config_file = tmpdir.join("config.json")
+    config_file.write(json.dumps(IMPERSONATED_IDENTITY_POOL_WORKFORCE_DATA))
+    monkeypatch.setenv(environment_vars.CREDENTIALS, str(config_file))
+
+    credentials, project_id = _default.default(
+        scopes=["https://www.google.com/calendar/feeds"]
+    )
+
+    assert isinstance(credentials, identity_pool.Credentials)
+    assert not credentials.is_user
+    assert credentials.is_workforce_pool
+    assert project_id is mock.sentinel.project_id
+    assert credentials.scopes == ["https://www.google.com/calendar/feeds"]
+
+
+@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
+def test_default_environ_external_credentials_with_user_and_default_scopes_and_quota_project_id(
+    get_project_id, monkeypatch, tmpdir
+):
+    config_file = tmpdir.join("config.json")
+    config_file.write(json.dumps(IDENTITY_POOL_DATA))
+    monkeypatch.setenv(environment_vars.CREDENTIALS, str(config_file))
+
+    credentials, project_id = _default.default(
+        scopes=["https://www.google.com/calendar/feeds"],
+        default_scopes=["https://www.googleapis.com/auth/cloud-platform"],
+        quota_project_id="project-foo",
+    )
+
+    assert isinstance(credentials, identity_pool.Credentials)
+    assert project_id is mock.sentinel.project_id
+    assert credentials.quota_project_id == "project-foo"
+    assert credentials.scopes == ["https://www.google.com/calendar/feeds"]
+    assert credentials.default_scopes == [
+        "https://www.googleapis.com/auth/cloud-platform"
+    ]
+
+
+@EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH
+def test_default_environ_external_credentials_explicit_request_with_scopes(
+    get_project_id, monkeypatch, tmpdir
+):
+    config_file = tmpdir.join("config.json")
+    config_file.write(json.dumps(IDENTITY_POOL_DATA))
+    monkeypatch.setenv(environment_vars.CREDENTIALS, str(config_file))
+
+    credentials, project_id = _default.default(
+        request=mock.sentinel.request,
+        scopes=["https://www.googleapis.com/auth/cloud-platform"],
+    )
+
+    assert isinstance(credentials, identity_pool.Credentials)
+    assert project_id is mock.sentinel.project_id
+    # default() will initialize new credentials via with_scopes_if_required
+    # and potentially with_quota_project.
+    # As a result the caller of get_project_id() will not match the returned
+    # credentials.
+    get_project_id.assert_called_with(mock.ANY, request=mock.sentinel.request)
+
+
+def test_default_environ_external_credentials_bad_format(monkeypatch, tmpdir):
+    filename = tmpdir.join("external_account_bad.json")
+    filename.write(json.dumps({"type": "external_account"}))
+    monkeypatch.setenv(environment_vars.CREDENTIALS, str(filename))
+
+    with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+        _default.default()
+
+    assert excinfo.match(
+        "Failed to load external account credentials from {}".format(str(filename))
+    )
+
+
[email protected](
+    "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True
+)
+def test_default_warning_without_quota_project_id_for_user_creds(get_adc_path):
+    get_adc_path.return_value = AUTHORIZED_USER_CLOUD_SDK_FILE
+
+    with pytest.warns(UserWarning, match="Cloud SDK"):
+        credentials, project_id = _default.default(quota_project_id=None)
+
+
[email protected](
+    "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True
+)
+def test_default_no_warning_with_quota_project_id_for_user_creds(get_adc_path):
+    get_adc_path.return_value = AUTHORIZED_USER_CLOUD_SDK_FILE
+
+    credentials, project_id = _default.default(quota_project_id="project-foo")
diff --git a/tests/test__helpers.py b/tests/test__helpers.py
new file mode 100644
index 0000000..0c0bad2
--- /dev/null
+++ b/tests/test__helpers.py
@@ -0,0 +1,170 @@
+# Copyright 2016 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 datetime
+
+import pytest
+from six.moves import urllib
+
+from google.auth import _helpers
+
+
+class SourceClass(object):
+    def func(self):  # pragma: NO COVER
+        """example docstring"""
+
+
+def test_copy_docstring_success():
+    def func():  # pragma: NO COVER
+        pass
+
+    _helpers.copy_docstring(SourceClass)(func)
+
+    assert func.__doc__ == SourceClass.func.__doc__
+
+
+def test_copy_docstring_conflict():
+    def func():  # pragma: NO COVER
+        """existing docstring"""
+        pass
+
+    with pytest.raises(ValueError):
+        _helpers.copy_docstring(SourceClass)(func)
+
+
+def test_copy_docstring_non_existing():
+    def func2():  # pragma: NO COVER
+        pass
+
+    with pytest.raises(AttributeError):
+        _helpers.copy_docstring(SourceClass)(func2)
+
+
+def test_utcnow():
+    assert isinstance(_helpers.utcnow(), datetime.datetime)
+
+
+def test_datetime_to_secs():
+    assert _helpers.datetime_to_secs(datetime.datetime(1970, 1, 1)) == 0
+    assert _helpers.datetime_to_secs(datetime.datetime(1990, 5, 29)) == 643939200
+
+
+def test_to_bytes_with_bytes():
+    value = b"bytes-val"
+    assert _helpers.to_bytes(value) == value
+
+
+def test_to_bytes_with_unicode():
+    value = u"string-val"
+    encoded_value = b"string-val"
+    assert _helpers.to_bytes(value) == encoded_value
+
+
+def test_to_bytes_with_nonstring_type():
+    with pytest.raises(ValueError):
+        _helpers.to_bytes(object())
+
+
+def test_from_bytes_with_unicode():
+    value = u"bytes-val"
+    assert _helpers.from_bytes(value) == value
+
+
+def test_from_bytes_with_bytes():
+    value = b"string-val"
+    decoded_value = u"string-val"
+    assert _helpers.from_bytes(value) == decoded_value
+
+
+def test_from_bytes_with_nonstring_type():
+    with pytest.raises(ValueError):
+        _helpers.from_bytes(object())
+
+
+def _assert_query(url, expected):
+    parts = urllib.parse.urlsplit(url)
+    query = urllib.parse.parse_qs(parts.query)
+    assert query == expected
+
+
+def test_update_query_params_no_params():
+    uri = "http://www.google.com"
+    updated = _helpers.update_query(uri, {"a": "b"})
+    assert updated == uri + "?a=b"
+
+
+def test_update_query_existing_params():
+    uri = "http://www.google.com?x=y"
+    updated = _helpers.update_query(uri, {"a": "b", "c": "d&"})
+    _assert_query(updated, {"x": ["y"], "a": ["b"], "c": ["d&"]})
+
+
+def test_update_query_replace_param():
+    base_uri = "http://www.google.com"
+    uri = base_uri + "?x=a"
+    updated = _helpers.update_query(uri, {"x": "b", "y": "c"})
+    _assert_query(updated, {"x": ["b"], "y": ["c"]})
+
+
+def test_update_query_remove_param():
+    base_uri = "http://www.google.com"
+    uri = base_uri + "?x=a"
+    updated = _helpers.update_query(uri, {"y": "c"}, remove=["x"])
+    _assert_query(updated, {"y": ["c"]})
+
+
+def test_scopes_to_string():
+    cases = [
+        ("", ()),
+        ("", []),
+        ("", ("",)),
+        ("", [""]),
+        ("a", ("a",)),
+        ("b", ["b"]),
+        ("a b", ["a", "b"]),
+        ("a b", ("a", "b")),
+        ("a b", (s for s in ["a", "b"])),
+    ]
+    for expected, case in cases:
+        assert _helpers.scopes_to_string(case) == expected
+
+
+def test_string_to_scopes():
+    cases = [("", []), ("a", ["a"]), ("a b c d e f", ["a", "b", "c", "d", "e", "f"])]
+
+    for case, expected in cases:
+        assert _helpers.string_to_scopes(case) == expected
+
+
+def test_padded_urlsafe_b64decode():
+    cases = [
+        ("YQ==", b"a"),
+        ("YQ", b"a"),
+        ("YWE=", b"aa"),
+        ("YWE", b"aa"),
+        ("YWFhYQ==", b"aaaa"),
+        ("YWFhYQ", b"aaaa"),
+        ("YWFhYWE=", b"aaaaa"),
+        ("YWFhYWE", b"aaaaa"),
+    ]
+
+    for case, expected in cases:
+        assert _helpers.padded_urlsafe_b64decode(case) == expected
+
+
+def test_unpadded_urlsafe_b64encode():
+    cases = [(b"", b""), (b"a", b"YQ"), (b"aa", b"YWE"), (b"aaa", b"YWFh")]
+
+    for case, expected in cases:
+        assert _helpers.unpadded_urlsafe_b64encode(case) == expected
diff --git a/tests/test__oauth2client.py b/tests/test__oauth2client.py
new file mode 100644
index 0000000..6b1112b
--- /dev/null
+++ b/tests/test__oauth2client.py
@@ -0,0 +1,170 @@
+# Copyright 2016 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 datetime
+import os
+import sys
+
+import mock
+import oauth2client.client
+import oauth2client.contrib.gce
+import oauth2client.service_account
+import pytest
+from six.moves import reload_module
+
+from google.auth import _oauth2client
+
+
+DATA_DIR = os.path.join(os.path.dirname(__file__), "data")
+SERVICE_ACCOUNT_JSON_FILE = os.path.join(DATA_DIR, "service_account.json")
+
+
+def test__convert_oauth2_credentials():
+    old_credentials = oauth2client.client.OAuth2Credentials(
+        "access_token",
+        "client_id",
+        "client_secret",
+        "refresh_token",
+        datetime.datetime.min,
+        "token_uri",
+        "user_agent",
+        scopes="one two",
+    )
+
+    new_credentials = _oauth2client._convert_oauth2_credentials(old_credentials)
+
+    assert new_credentials.token == old_credentials.access_token
+    assert new_credentials._refresh_token == old_credentials.refresh_token
+    assert new_credentials._client_id == old_credentials.client_id
+    assert new_credentials._client_secret == old_credentials.client_secret
+    assert new_credentials._token_uri == old_credentials.token_uri
+    assert new_credentials.scopes == old_credentials.scopes
+
+
+def test__convert_service_account_credentials():
+    old_class = oauth2client.service_account.ServiceAccountCredentials
+    old_credentials = old_class.from_json_keyfile_name(SERVICE_ACCOUNT_JSON_FILE)
+
+    new_credentials = _oauth2client._convert_service_account_credentials(
+        old_credentials
+    )
+
+    assert (
+        new_credentials.service_account_email == old_credentials.service_account_email
+    )
+    assert new_credentials._signer.key_id == old_credentials._private_key_id
+    assert new_credentials._token_uri == old_credentials.token_uri
+
+
+def test__convert_service_account_credentials_with_jwt():
+    old_class = oauth2client.service_account._JWTAccessCredentials
+    old_credentials = old_class.from_json_keyfile_name(SERVICE_ACCOUNT_JSON_FILE)
+
+    new_credentials = _oauth2client._convert_service_account_credentials(
+        old_credentials
+    )
+
+    assert (
+        new_credentials.service_account_email == old_credentials.service_account_email
+    )
+    assert new_credentials._signer.key_id == old_credentials._private_key_id
+    assert new_credentials._token_uri == old_credentials.token_uri
+
+
+def test__convert_gce_app_assertion_credentials():
+    old_credentials = oauth2client.contrib.gce.AppAssertionCredentials(
+        email="some_email"
+    )
+
+    new_credentials = _oauth2client._convert_gce_app_assertion_credentials(
+        old_credentials
+    )
+
+    assert (
+        new_credentials.service_account_email == old_credentials.service_account_email
+    )
+
+
[email protected]
+def mock_oauth2client_gae_imports(mock_non_existent_module):
+    mock_non_existent_module("google.appengine.api.app_identity")
+    mock_non_existent_module("google.appengine.ext.ndb")
+    mock_non_existent_module("google.appengine.ext.webapp.util")
+    mock_non_existent_module("webapp2")
+
+
[email protected]("google.auth.app_engine.app_identity")
+def test__convert_appengine_app_assertion_credentials(
+    app_identity, mock_oauth2client_gae_imports
+):
+
+    import oauth2client.contrib.appengine
+
+    service_account_id = "service_account_id"
+    old_credentials = oauth2client.contrib.appengine.AppAssertionCredentials(
+        scope="one two", service_account_id=service_account_id
+    )
+
+    new_credentials = _oauth2client._convert_appengine_app_assertion_credentials(
+        old_credentials
+    )
+
+    assert new_credentials.scopes == ["one", "two"]
+    assert new_credentials._service_account_id == old_credentials.service_account_id
+
+
+class FakeCredentials(object):
+    pass
+
+
+def test_convert_success():
+    convert_function = mock.Mock(spec=["__call__"])
+    conversion_map_patch = mock.patch.object(
+        _oauth2client, "_CLASS_CONVERSION_MAP", {FakeCredentials: convert_function}
+    )
+    credentials = FakeCredentials()
+
+    with conversion_map_patch:
+        result = _oauth2client.convert(credentials)
+
+    convert_function.assert_called_once_with(credentials)
+    assert result == convert_function.return_value
+
+
+def test_convert_not_found():
+    with pytest.raises(ValueError) as excinfo:
+        _oauth2client.convert("a string is not a real credentials class")
+
+    assert excinfo.match("Unable to convert")
+
+
[email protected]
+def reset__oauth2client_module():
+    """Reloads the _oauth2client module after a test."""
+    reload_module(_oauth2client)
+
+
+def test_import_has_app_engine(
+    mock_oauth2client_gae_imports, reset__oauth2client_module
+):
+    reload_module(_oauth2client)
+    assert _oauth2client._HAS_APPENGINE
+
+
+def test_import_without_oauth2client(monkeypatch, reset__oauth2client_module):
+    monkeypatch.setitem(sys.modules, "oauth2client", None)
+    with pytest.raises(ImportError) as excinfo:
+        reload_module(_oauth2client)
+
+    assert excinfo.match("oauth2client")
diff --git a/tests/test__service_account_info.py b/tests/test__service_account_info.py
new file mode 100644
index 0000000..13b2f85
--- /dev/null
+++ b/tests/test__service_account_info.py
@@ -0,0 +1,62 @@
+# Copyright 2016 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 json
+import os
+
+import pytest
+import six
+
+from google.auth import _service_account_info
+from google.auth import crypt
+
+
+DATA_DIR = os.path.join(os.path.dirname(__file__), "data")
+SERVICE_ACCOUNT_JSON_FILE = os.path.join(DATA_DIR, "service_account.json")
+
+with open(SERVICE_ACCOUNT_JSON_FILE, "r") as fh:
+    SERVICE_ACCOUNT_INFO = json.load(fh)
+
+
+def test_from_dict():
+    signer = _service_account_info.from_dict(SERVICE_ACCOUNT_INFO)
+    assert isinstance(signer, crypt.RSASigner)
+    assert signer.key_id == SERVICE_ACCOUNT_INFO["private_key_id"]
+
+
+def test_from_dict_bad_private_key():
+    info = SERVICE_ACCOUNT_INFO.copy()
+    info["private_key"] = "garbage"
+
+    with pytest.raises(ValueError) as excinfo:
+        _service_account_info.from_dict(info)
+
+    assert excinfo.match(r"key")
+
+
+def test_from_dict_bad_format():
+    with pytest.raises(ValueError) as excinfo:
+        _service_account_info.from_dict({}, require=("meep",))
+
+    assert excinfo.match(r"missing fields")
+
+
+def test_from_filename():
+    info, signer = _service_account_info.from_filename(SERVICE_ACCOUNT_JSON_FILE)
+
+    for key, value in six.iteritems(SERVICE_ACCOUNT_INFO):
+        assert info[key] == value
+
+    assert isinstance(signer, crypt.RSASigner)
+    assert signer.key_id == SERVICE_ACCOUNT_INFO["private_key_id"]
diff --git a/tests/test_app_engine.py b/tests/test_app_engine.py
new file mode 100644
index 0000000..6a788b9
--- /dev/null
+++ b/tests/test_app_engine.py
@@ -0,0 +1,217 @@
+# Copyright 2016 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 datetime
+
+import mock
+import pytest
+
+from google.auth import app_engine
+
+
+class _AppIdentityModule(object):
+    """The interface of the App Idenity app engine module.
+    See https://cloud.google.com/appengine/docs/standard/python/refdocs
+    /google.appengine.api.app_identity.app_identity
+    """
+
+    def get_application_id(self):
+        raise NotImplementedError()
+
+    def sign_blob(self, bytes_to_sign, deadline=None):
+        raise NotImplementedError()
+
+    def get_service_account_name(self, deadline=None):
+        raise NotImplementedError()
+
+    def get_access_token(self, scopes, service_account_id=None):
+        raise NotImplementedError()
+
+
[email protected]
+def app_identity(monkeypatch):
+    """Mocks the app_identity module for google.auth.app_engine."""
+    app_identity_module = mock.create_autospec(_AppIdentityModule, instance=True)
+    monkeypatch.setattr(app_engine, "app_identity", app_identity_module)
+    yield app_identity_module
+
+
+def test_get_project_id(app_identity):
+    app_identity.get_application_id.return_value = mock.sentinel.project
+    assert app_engine.get_project_id() == mock.sentinel.project
+
+
[email protected](app_engine, "app_identity", new=None)
+def test_get_project_id_missing_apis():
+    with pytest.raises(EnvironmentError) as excinfo:
+        assert app_engine.get_project_id()
+
+    assert excinfo.match(r"App Engine APIs are not available")
+
+
+class TestSigner(object):
+    def test_key_id(self, app_identity):
+        app_identity.sign_blob.return_value = (
+            mock.sentinel.key_id,
+            mock.sentinel.signature,
+        )
+
+        signer = app_engine.Signer()
+
+        assert signer.key_id is None
+
+    def test_sign(self, app_identity):
+        app_identity.sign_blob.return_value = (
+            mock.sentinel.key_id,
+            mock.sentinel.signature,
+        )
+
+        signer = app_engine.Signer()
+        to_sign = b"123"
+
+        signature = signer.sign(to_sign)
+
+        assert signature == mock.sentinel.signature
+        app_identity.sign_blob.assert_called_with(to_sign)
+
+
+class TestCredentials(object):
+    @mock.patch.object(app_engine, "app_identity", new=None)
+    def test_missing_apis(self):
+        with pytest.raises(EnvironmentError) as excinfo:
+            app_engine.Credentials()
+
+        assert excinfo.match(r"App Engine APIs are not available")
+
+    def test_default_state(self, app_identity):
+        credentials = app_engine.Credentials()
+
+        # Not token acquired yet
+        assert not credentials.valid
+        # Expiration hasn't been set yet
+        assert not credentials.expired
+        # Scopes are required
+        assert not credentials.scopes
+        assert not credentials.default_scopes
+        assert credentials.requires_scopes
+        assert not credentials.quota_project_id
+
+    def test_with_scopes(self, app_identity):
+        credentials = app_engine.Credentials()
+
+        assert not credentials.scopes
+        assert credentials.requires_scopes
+
+        scoped_credentials = credentials.with_scopes(["email"])
+
+        assert scoped_credentials.has_scopes(["email"])
+        assert not scoped_credentials.requires_scopes
+
+    def test_with_default_scopes(self, app_identity):
+        credentials = app_engine.Credentials()
+
+        assert not credentials.scopes
+        assert not credentials.default_scopes
+        assert credentials.requires_scopes
+
+        scoped_credentials = credentials.with_scopes(
+            scopes=None, default_scopes=["email"]
+        )
+
+        assert scoped_credentials.has_scopes(["email"])
+        assert not scoped_credentials.requires_scopes
+
+    def test_with_quota_project(self, app_identity):
+        credentials = app_engine.Credentials()
+
+        assert not credentials.scopes
+        assert not credentials.quota_project_id
+
+        quota_project_creds = credentials.with_quota_project("project-foo")
+
+        assert quota_project_creds.quota_project_id == "project-foo"
+
+    def test_service_account_email_implicit(self, app_identity):
+        app_identity.get_service_account_name.return_value = (
+            mock.sentinel.service_account_email
+        )
+        credentials = app_engine.Credentials()
+
+        assert credentials.service_account_email == mock.sentinel.service_account_email
+        assert app_identity.get_service_account_name.called
+
+    def test_service_account_email_explicit(self, app_identity):
+        credentials = app_engine.Credentials(
+            service_account_id=mock.sentinel.service_account_email
+        )
+
+        assert credentials.service_account_email == mock.sentinel.service_account_email
+        assert not app_identity.get_service_account_name.called
+
+    @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
+    def test_refresh(self, utcnow, app_identity):
+        token = "token"
+        ttl = 643942923
+        app_identity.get_access_token.return_value = token, ttl
+        credentials = app_engine.Credentials(
+            scopes=["email"], default_scopes=["profile"]
+        )
+
+        credentials.refresh(None)
+
+        app_identity.get_access_token.assert_called_with(
+            credentials.scopes, credentials._service_account_id
+        )
+        assert credentials.token == token
+        assert credentials.expiry == datetime.datetime(1990, 5, 29, 1, 2, 3)
+        assert credentials.valid
+        assert not credentials.expired
+
+    @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
+    def test_refresh_with_default_scopes(self, utcnow, app_identity):
+        token = "token"
+        ttl = 643942923
+        app_identity.get_access_token.return_value = token, ttl
+        credentials = app_engine.Credentials(default_scopes=["email"])
+
+        credentials.refresh(None)
+
+        app_identity.get_access_token.assert_called_with(
+            credentials.default_scopes, credentials._service_account_id
+        )
+        assert credentials.token == token
+        assert credentials.expiry == datetime.datetime(1990, 5, 29, 1, 2, 3)
+        assert credentials.valid
+        assert not credentials.expired
+
+    def test_sign_bytes(self, app_identity):
+        app_identity.sign_blob.return_value = (
+            mock.sentinel.key_id,
+            mock.sentinel.signature,
+        )
+        credentials = app_engine.Credentials()
+        to_sign = b"123"
+
+        signature = credentials.sign_bytes(to_sign)
+
+        assert signature == mock.sentinel.signature
+        app_identity.sign_blob.assert_called_with(to_sign)
+
+    def test_signer(self, app_identity):
+        credentials = app_engine.Credentials()
+        assert isinstance(credentials.signer, app_engine.Signer)
+
+    def test_signer_email(self, app_identity):
+        credentials = app_engine.Credentials()
+        assert credentials.signer_email == credentials.service_account_email
diff --git a/tests/test_aws.py b/tests/test_aws.py
new file mode 100644
index 0000000..9ca08d5
--- /dev/null
+++ b/tests/test_aws.py
@@ -0,0 +1,1497 @@
+# 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.
+
+import datetime
+import json
+
+import mock
+import pytest
+from six.moves import http_client
+from six.moves import urllib
+
+from google.auth import _helpers
+from google.auth import aws
+from google.auth import environment_vars
+from google.auth import exceptions
+from google.auth import transport
+
+
+CLIENT_ID = "username"
+CLIENT_SECRET = "password"
+# Base64 encoding of "username:password".
+BASIC_AUTH_ENCODING = "dXNlcm5hbWU6cGFzc3dvcmQ="
+SERVICE_ACCOUNT_EMAIL = "[email protected]"
+SERVICE_ACCOUNT_IMPERSONATION_URL = (
+    "https://us-east1-iamcredentials.googleapis.com/v1/projects/-"
+    + "/serviceAccounts/{}:generateAccessToken".format(SERVICE_ACCOUNT_EMAIL)
+)
+QUOTA_PROJECT_ID = "QUOTA_PROJECT_ID"
+SCOPES = ["scope1", "scope2"]
+TOKEN_URL = "https://sts.googleapis.com/v1/token"
+SUBJECT_TOKEN_TYPE = "urn:ietf:params:aws:token-type:aws4_request"
+AUDIENCE = "//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID"
+REGION_URL = "http://169.254.169.254/latest/meta-data/placement/availability-zone"
+SECURITY_CREDS_URL = "http://169.254.169.254/latest/meta-data/iam/security-credentials"
+CRED_VERIFICATION_URL = (
+    "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15"
+)
+# Sample AWS security credentials to be used with tests that require a session token.
+ACCESS_KEY_ID = "ASIARD4OQDT6A77FR3CL"
+SECRET_ACCESS_KEY = "Y8AfSaucF37G4PpvfguKZ3/l7Id4uocLXxX0+VTx"
+TOKEN = "IQoJb3JpZ2luX2VjEIz//////////wEaCXVzLWVhc3QtMiJGMEQCIH7MHX/Oy/OB8OlLQa9GrqU1B914+iMikqWQW7vPCKlgAiA/Lsv8Jcafn14owfxXn95FURZNKaaphj0ykpmS+Ki+CSq0AwhlEAAaDDA3NzA3MTM5MTk5NiIMx9sAeP1ovlMTMKLjKpEDwuJQg41/QUKx0laTZYjPlQvjwSqS3OB9P1KAXPWSLkliVMMqaHqelvMF/WO/glv3KwuTfQsavRNs3v5pcSEm4SPO3l7mCs7KrQUHwGP0neZhIKxEXy+Ls//1C/Bqt53NL+LSbaGv6RPHaX82laz2qElphg95aVLdYgIFY6JWV5fzyjgnhz0DQmy62/Vi8pNcM2/VnxeCQ8CC8dRDSt52ry2v+nc77vstuI9xV5k8mPtnaPoJDRANh0bjwY5Sdwkbp+mGRUJBAQRlNgHUJusefXQgVKBCiyJY4w3Csd8Bgj9IyDV+Azuy1jQqfFZWgP68LSz5bURyIjlWDQunO82stZ0BgplKKAa/KJHBPCp8Qi6i99uy7qh76FQAqgVTsnDuU6fGpHDcsDSGoCls2HgZjZFPeOj8mmRhFk1Xqvkbjuz8V1cJk54d3gIJvQt8gD2D6yJQZecnuGWd5K2e2HohvCc8Fc9kBl1300nUJPV+k4tr/A5R/0QfEKOZL1/k5lf1g9CREnrM8LVkGxCgdYMxLQow1uTL+QU67AHRRSp5PhhGX4Rek+01vdYSnJCMaPhSEgcLqDlQkhk6MPsyT91QMXcWmyO+cAZwUPwnRamFepuP4K8k2KVXs/LIJHLELwAZ0ekyaS7CptgOqS7uaSTFG3U+vzFZLEnGvWQ7y9IPNQZ+Dffgh4p3vF4J68y9049sI6Sr5d5wbKkcbm8hdCDHZcv4lnqohquPirLiFQ3q7B17V9krMPu3mz1cg4Ekgcrn/E09NTsxAqD8NcZ7C7ECom9r+X3zkDOxaajW6hu3Az8hGlyylDaMiFfRbBJpTIlxp7jfa7CxikNgNtEKLH9iCzvuSg2vhA=="
+# To avoid json.dumps() differing behavior from one version to other,
+# the JSON payload is hardcoded.
+REQUEST_PARAMS = '{"KeySchema":[{"KeyType":"HASH","AttributeName":"Id"}],"TableName":"TestTable","AttributeDefinitions":[{"AttributeName":"Id","AttributeType":"S"}],"ProvisionedThroughput":{"WriteCapacityUnits":5,"ReadCapacityUnits":5}}'
+# Each tuple contains the following entries:
+# region, time, credentials, original_request, signed_request
+TEST_FIXTURES = [
+    # GET request (AWS botocore tests).
+    # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla.req
+    # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla.sreq
+    (
+        "us-east-1",
+        "2011-09-09T23:36:00Z",
+        {
+            "access_key_id": "AKIDEXAMPLE",
+            "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
+        },
+        {
+            "method": "GET",
+            "url": "https://host.foo.com",
+            "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT"},
+        },
+        {
+            "url": "https://host.foo.com",
+            "method": "GET",
+            "headers": {
+                "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=b27ccfbfa7df52a200ff74193ca6e32d4b48b8856fab7ebf1c595d0670a7e470",
+                "host": "host.foo.com",
+                "date": "Mon, 09 Sep 2011 23:36:00 GMT",
+            },
+        },
+    ),
+    # GET request with relative path (AWS botocore tests).
+    # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-relative-relative.req
+    # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-relative-relative.sreq
+    (
+        "us-east-1",
+        "2011-09-09T23:36:00Z",
+        {
+            "access_key_id": "AKIDEXAMPLE",
+            "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
+        },
+        {
+            "method": "GET",
+            "url": "https://host.foo.com/foo/bar/../..",
+            "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT"},
+        },
+        {
+            "url": "https://host.foo.com/foo/bar/../..",
+            "method": "GET",
+            "headers": {
+                "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=b27ccfbfa7df52a200ff74193ca6e32d4b48b8856fab7ebf1c595d0670a7e470",
+                "host": "host.foo.com",
+                "date": "Mon, 09 Sep 2011 23:36:00 GMT",
+            },
+        },
+    ),
+    # GET request with /./ path (AWS botocore tests).
+    # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-slash-dot-slash.req
+    # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-slash-dot-slash.sreq
+    (
+        "us-east-1",
+        "2011-09-09T23:36:00Z",
+        {
+            "access_key_id": "AKIDEXAMPLE",
+            "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
+        },
+        {
+            "method": "GET",
+            "url": "https://host.foo.com/./",
+            "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT"},
+        },
+        {
+            "url": "https://host.foo.com/./",
+            "method": "GET",
+            "headers": {
+                "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=b27ccfbfa7df52a200ff74193ca6e32d4b48b8856fab7ebf1c595d0670a7e470",
+                "host": "host.foo.com",
+                "date": "Mon, 09 Sep 2011 23:36:00 GMT",
+            },
+        },
+    ),
+    # GET request with pointless dot path (AWS botocore tests).
+    # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-slash-pointless-dot.req
+    # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-slash-pointless-dot.sreq
+    (
+        "us-east-1",
+        "2011-09-09T23:36:00Z",
+        {
+            "access_key_id": "AKIDEXAMPLE",
+            "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
+        },
+        {
+            "method": "GET",
+            "url": "https://host.foo.com/./foo",
+            "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT"},
+        },
+        {
+            "url": "https://host.foo.com/./foo",
+            "method": "GET",
+            "headers": {
+                "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=910e4d6c9abafaf87898e1eb4c929135782ea25bb0279703146455745391e63a",
+                "host": "host.foo.com",
+                "date": "Mon, 09 Sep 2011 23:36:00 GMT",
+            },
+        },
+    ),
+    # GET request with utf8 path (AWS botocore tests).
+    # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-utf8.req
+    # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-utf8.sreq
+    (
+        "us-east-1",
+        "2011-09-09T23:36:00Z",
+        {
+            "access_key_id": "AKIDEXAMPLE",
+            "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
+        },
+        {
+            "method": "GET",
+            "url": "https://host.foo.com/%E1%88%B4",
+            "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT"},
+        },
+        {
+            "url": "https://host.foo.com/%E1%88%B4",
+            "method": "GET",
+            "headers": {
+                "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=8d6634c189aa8c75c2e51e106b6b5121bed103fdb351f7d7d4381c738823af74",
+                "host": "host.foo.com",
+                "date": "Mon, 09 Sep 2011 23:36:00 GMT",
+            },
+        },
+    ),
+    # GET request with duplicate query key (AWS botocore tests).
+    # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla-query-order-key-case.req
+    # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla-query-order-key-case.sreq
+    (
+        "us-east-1",
+        "2011-09-09T23:36:00Z",
+        {
+            "access_key_id": "AKIDEXAMPLE",
+            "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
+        },
+        {
+            "method": "GET",
+            "url": "https://host.foo.com/?foo=Zoo&foo=aha",
+            "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT"},
+        },
+        {
+            "url": "https://host.foo.com/?foo=Zoo&foo=aha",
+            "method": "GET",
+            "headers": {
+                "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=be7148d34ebccdc6423b19085378aa0bee970bdc61d144bd1a8c48c33079ab09",
+                "host": "host.foo.com",
+                "date": "Mon, 09 Sep 2011 23:36:00 GMT",
+            },
+        },
+    ),
+    # GET request with duplicate out of order query key (AWS botocore tests).
+    # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla-query-order-value.req
+    # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla-query-order-value.sreq
+    (
+        "us-east-1",
+        "2011-09-09T23:36:00Z",
+        {
+            "access_key_id": "AKIDEXAMPLE",
+            "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
+        },
+        {
+            "method": "GET",
+            "url": "https://host.foo.com/?foo=b&foo=a",
+            "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT"},
+        },
+        {
+            "url": "https://host.foo.com/?foo=b&foo=a",
+            "method": "GET",
+            "headers": {
+                "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=feb926e49e382bec75c9d7dcb2a1b6dc8aa50ca43c25d2bc51143768c0875acc",
+                "host": "host.foo.com",
+                "date": "Mon, 09 Sep 2011 23:36:00 GMT",
+            },
+        },
+    ),
+    # GET request with utf8 query (AWS botocore tests).
+    # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla-ut8-query.req
+    # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla-ut8-query.sreq
+    (
+        "us-east-1",
+        "2011-09-09T23:36:00Z",
+        {
+            "access_key_id": "AKIDEXAMPLE",
+            "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
+        },
+        {
+            "method": "GET",
+            "url": "https://host.foo.com/?{}=bar".format(
+                urllib.parse.unquote("%E1%88%B4")
+            ),
+            "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT"},
+        },
+        {
+            "url": "https://host.foo.com/?{}=bar".format(
+                urllib.parse.unquote("%E1%88%B4")
+            ),
+            "method": "GET",
+            "headers": {
+                "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=6fb359e9a05394cc7074e0feb42573a2601abc0c869a953e8c5c12e4e01f1a8c",
+                "host": "host.foo.com",
+                "date": "Mon, 09 Sep 2011 23:36:00 GMT",
+            },
+        },
+    ),
+    # POST request with sorted headers (AWS botocore tests).
+    # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-header-key-sort.req
+    # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-header-key-sort.sreq
+    (
+        "us-east-1",
+        "2011-09-09T23:36:00Z",
+        {
+            "access_key_id": "AKIDEXAMPLE",
+            "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
+        },
+        {
+            "method": "POST",
+            "url": "https://host.foo.com/",
+            "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT", "ZOO": "zoobar"},
+        },
+        {
+            "url": "https://host.foo.com/",
+            "method": "POST",
+            "headers": {
+                "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host;zoo, Signature=b7a95a52518abbca0964a999a880429ab734f35ebbf1235bd79a5de87756dc4a",
+                "host": "host.foo.com",
+                "date": "Mon, 09 Sep 2011 23:36:00 GMT",
+                "ZOO": "zoobar",
+            },
+        },
+    ),
+    # POST request with upper case header value from AWS Python test harness.
+    # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-header-value-case.req
+    # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-header-value-case.sreq
+    (
+        "us-east-1",
+        "2011-09-09T23:36:00Z",
+        {
+            "access_key_id": "AKIDEXAMPLE",
+            "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
+        },
+        {
+            "method": "POST",
+            "url": "https://host.foo.com/",
+            "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT", "zoo": "ZOOBAR"},
+        },
+        {
+            "url": "https://host.foo.com/",
+            "method": "POST",
+            "headers": {
+                "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host;zoo, Signature=273313af9d0c265c531e11db70bbd653f3ba074c1009239e8559d3987039cad7",
+                "host": "host.foo.com",
+                "date": "Mon, 09 Sep 2011 23:36:00 GMT",
+                "zoo": "ZOOBAR",
+            },
+        },
+    ),
+    # POST request with header and no body (AWS botocore tests).
+    # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-header-value-trim.req
+    # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-header-value-trim.sreq
+    (
+        "us-east-1",
+        "2011-09-09T23:36:00Z",
+        {
+            "access_key_id": "AKIDEXAMPLE",
+            "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
+        },
+        {
+            "method": "POST",
+            "url": "https://host.foo.com/",
+            "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT", "p": "phfft"},
+        },
+        {
+            "url": "https://host.foo.com/",
+            "method": "POST",
+            "headers": {
+                "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host;p, Signature=debf546796015d6f6ded8626f5ce98597c33b47b9164cf6b17b4642036fcb592",
+                "host": "host.foo.com",
+                "date": "Mon, 09 Sep 2011 23:36:00 GMT",
+                "p": "phfft",
+            },
+        },
+    ),
+    # POST request with body and no header (AWS botocore tests).
+    # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-x-www-form-urlencoded.req
+    # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-x-www-form-urlencoded.sreq
+    (
+        "us-east-1",
+        "2011-09-09T23:36:00Z",
+        {
+            "access_key_id": "AKIDEXAMPLE",
+            "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
+        },
+        {
+            "method": "POST",
+            "url": "https://host.foo.com/",
+            "headers": {
+                "Content-Type": "application/x-www-form-urlencoded",
+                "date": "Mon, 09 Sep 2011 23:36:00 GMT",
+            },
+            "data": "foo=bar",
+        },
+        {
+            "url": "https://host.foo.com/",
+            "method": "POST",
+            "headers": {
+                "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=content-type;date;host, Signature=5a15b22cf462f047318703b92e6f4f38884e4a7ab7b1d6426ca46a8bd1c26cbc",
+                "host": "host.foo.com",
+                "Content-Type": "application/x-www-form-urlencoded",
+                "date": "Mon, 09 Sep 2011 23:36:00 GMT",
+            },
+            "data": "foo=bar",
+        },
+    ),
+    # POST request with querystring (AWS botocore tests).
+    # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-vanilla-query.req
+    # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-vanilla-query.sreq
+    (
+        "us-east-1",
+        "2011-09-09T23:36:00Z",
+        {
+            "access_key_id": "AKIDEXAMPLE",
+            "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
+        },
+        {
+            "method": "POST",
+            "url": "https://host.foo.com/?foo=bar",
+            "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT"},
+        },
+        {
+            "url": "https://host.foo.com/?foo=bar",
+            "method": "POST",
+            "headers": {
+                "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=b6e3b79003ce0743a491606ba1035a804593b0efb1e20a11cba83f8c25a57a92",
+                "host": "host.foo.com",
+                "date": "Mon, 09 Sep 2011 23:36:00 GMT",
+            },
+        },
+    ),
+    # GET request with session token credentials.
+    (
+        "us-east-2",
+        "2020-08-11T06:55:22Z",
+        {
+            "access_key_id": ACCESS_KEY_ID,
+            "secret_access_key": SECRET_ACCESS_KEY,
+            "security_token": TOKEN,
+        },
+        {
+            "method": "GET",
+            "url": "https://ec2.us-east-2.amazonaws.com?Action=DescribeRegions&Version=2013-10-15",
+        },
+        {
+            "url": "https://ec2.us-east-2.amazonaws.com?Action=DescribeRegions&Version=2013-10-15",
+            "method": "GET",
+            "headers": {
+                "Authorization": "AWS4-HMAC-SHA256 Credential="
+                + ACCESS_KEY_ID
+                + "/20200811/us-east-2/ec2/aws4_request, SignedHeaders=host;x-amz-date;x-amz-security-token, Signature=631ea80cddfaa545fdadb120dc92c9f18166e38a5c47b50fab9fce476e022855",
+                "host": "ec2.us-east-2.amazonaws.com",
+                "x-amz-date": "20200811T065522Z",
+                "x-amz-security-token": TOKEN,
+            },
+        },
+    ),
+    # POST request with session token credentials.
+    (
+        "us-east-2",
+        "2020-08-11T06:55:22Z",
+        {
+            "access_key_id": ACCESS_KEY_ID,
+            "secret_access_key": SECRET_ACCESS_KEY,
+            "security_token": TOKEN,
+        },
+        {
+            "method": "POST",
+            "url": "https://sts.us-east-2.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15",
+        },
+        {
+            "url": "https://sts.us-east-2.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15",
+            "method": "POST",
+            "headers": {
+                "Authorization": "AWS4-HMAC-SHA256 Credential="
+                + ACCESS_KEY_ID
+                + "/20200811/us-east-2/sts/aws4_request, SignedHeaders=host;x-amz-date;x-amz-security-token, Signature=73452984e4a880ffdc5c392355733ec3f5ba310d5e0609a89244440cadfe7a7a",
+                "host": "sts.us-east-2.amazonaws.com",
+                "x-amz-date": "20200811T065522Z",
+                "x-amz-security-token": TOKEN,
+            },
+        },
+    ),
+    # POST request with computed x-amz-date and no data.
+    (
+        "us-east-2",
+        "2020-08-11T06:55:22Z",
+        {"access_key_id": ACCESS_KEY_ID, "secret_access_key": SECRET_ACCESS_KEY},
+        {
+            "method": "POST",
+            "url": "https://sts.us-east-2.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15",
+        },
+        {
+            "url": "https://sts.us-east-2.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15",
+            "method": "POST",
+            "headers": {
+                "Authorization": "AWS4-HMAC-SHA256 Credential="
+                + ACCESS_KEY_ID
+                + "/20200811/us-east-2/sts/aws4_request, SignedHeaders=host;x-amz-date, Signature=d095ba304919cd0d5570ba8a3787884ee78b860f268ed040ba23831d55536d56",
+                "host": "sts.us-east-2.amazonaws.com",
+                "x-amz-date": "20200811T065522Z",
+            },
+        },
+    ),
+    # POST request with session token and additional headers/data.
+    (
+        "us-east-2",
+        "2020-08-11T06:55:22Z",
+        {
+            "access_key_id": ACCESS_KEY_ID,
+            "secret_access_key": SECRET_ACCESS_KEY,
+            "security_token": TOKEN,
+        },
+        {
+            "method": "POST",
+            "url": "https://dynamodb.us-east-2.amazonaws.com/",
+            "headers": {
+                "Content-Type": "application/x-amz-json-1.0",
+                "x-amz-target": "DynamoDB_20120810.CreateTable",
+            },
+            "data": REQUEST_PARAMS,
+        },
+        {
+            "url": "https://dynamodb.us-east-2.amazonaws.com/",
+            "method": "POST",
+            "headers": {
+                "Authorization": "AWS4-HMAC-SHA256 Credential="
+                + ACCESS_KEY_ID
+                + "/20200811/us-east-2/dynamodb/aws4_request, SignedHeaders=content-type;host;x-amz-date;x-amz-security-token;x-amz-target, Signature=fdaa5b9cc9c86b80fe61eaf504141c0b3523780349120f2bd8145448456e0385",
+                "host": "dynamodb.us-east-2.amazonaws.com",
+                "x-amz-date": "20200811T065522Z",
+                "Content-Type": "application/x-amz-json-1.0",
+                "x-amz-target": "DynamoDB_20120810.CreateTable",
+                "x-amz-security-token": TOKEN,
+            },
+            "data": REQUEST_PARAMS,
+        },
+    ),
+]
+
+
+class TestRequestSigner(object):
+    @pytest.mark.parametrize(
+        "region, time, credentials, original_request, signed_request", TEST_FIXTURES
+    )
+    @mock.patch("google.auth._helpers.utcnow")
+    def test_get_request_options(
+        self, utcnow, region, time, credentials, original_request, signed_request
+    ):
+        utcnow.return_value = datetime.datetime.strptime(time, "%Y-%m-%dT%H:%M:%SZ")
+        request_signer = aws.RequestSigner(region)
+        actual_signed_request = request_signer.get_request_options(
+            credentials,
+            original_request.get("url"),
+            original_request.get("method"),
+            original_request.get("data"),
+            original_request.get("headers"),
+        )
+
+        assert actual_signed_request == signed_request
+
+    def test_get_request_options_with_missing_scheme_url(self):
+        request_signer = aws.RequestSigner("us-east-2")
+
+        with pytest.raises(ValueError) as excinfo:
+            request_signer.get_request_options(
+                {
+                    "access_key_id": ACCESS_KEY_ID,
+                    "secret_access_key": SECRET_ACCESS_KEY,
+                },
+                "invalid",
+                "POST",
+            )
+
+        assert excinfo.match(r"Invalid AWS service URL")
+
+    def test_get_request_options_with_invalid_scheme_url(self):
+        request_signer = aws.RequestSigner("us-east-2")
+
+        with pytest.raises(ValueError) as excinfo:
+            request_signer.get_request_options(
+                {
+                    "access_key_id": ACCESS_KEY_ID,
+                    "secret_access_key": SECRET_ACCESS_KEY,
+                },
+                "http://invalid",
+                "POST",
+            )
+
+        assert excinfo.match(r"Invalid AWS service URL")
+
+    def test_get_request_options_with_missing_hostname_url(self):
+        request_signer = aws.RequestSigner("us-east-2")
+
+        with pytest.raises(ValueError) as excinfo:
+            request_signer.get_request_options(
+                {
+                    "access_key_id": ACCESS_KEY_ID,
+                    "secret_access_key": SECRET_ACCESS_KEY,
+                },
+                "https://",
+                "POST",
+            )
+
+        assert excinfo.match(r"Invalid AWS service URL")
+
+
+class TestCredentials(object):
+    AWS_REGION = "us-east-2"
+    AWS_ROLE = "gcp-aws-role"
+    AWS_SECURITY_CREDENTIALS_RESPONSE = {
+        "AccessKeyId": ACCESS_KEY_ID,
+        "SecretAccessKey": SECRET_ACCESS_KEY,
+        "Token": TOKEN,
+    }
+    AWS_SIGNATURE_TIME = "2020-08-11T06:55:22Z"
+    CREDENTIAL_SOURCE = {
+        "environment_id": "aws1",
+        "region_url": REGION_URL,
+        "url": SECURITY_CREDS_URL,
+        "regional_cred_verification_url": CRED_VERIFICATION_URL,
+    }
+    SUCCESS_RESPONSE = {
+        "access_token": "ACCESS_TOKEN",
+        "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
+        "token_type": "Bearer",
+        "expires_in": 3600,
+        "scope": " ".join(SCOPES),
+    }
+
+    @classmethod
+    def make_serialized_aws_signed_request(
+        cls,
+        aws_security_credentials,
+        region_name="us-east-2",
+        url="https://sts.us-east-2.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15",
+    ):
+        """Utility to generate serialize AWS signed requests.
+        This makes it easy to assert generated subject tokens based on the
+        provided AWS security credentials, regions and AWS STS endpoint.
+        """
+        request_signer = aws.RequestSigner(region_name)
+        signed_request = request_signer.get_request_options(
+            aws_security_credentials, url, "POST"
+        )
+        reformatted_signed_request = {
+            "url": signed_request.get("url"),
+            "method": signed_request.get("method"),
+            "headers": [
+                {
+                    "key": "Authorization",
+                    "value": signed_request.get("headers").get("Authorization"),
+                },
+                {"key": "host", "value": signed_request.get("headers").get("host")},
+                {
+                    "key": "x-amz-date",
+                    "value": signed_request.get("headers").get("x-amz-date"),
+                },
+            ],
+        }
+        # Include security token if available.
+        if "security_token" in aws_security_credentials:
+            reformatted_signed_request.get("headers").append(
+                {
+                    "key": "x-amz-security-token",
+                    "value": signed_request.get("headers").get("x-amz-security-token"),
+                }
+            )
+        # Append x-goog-cloud-target-resource header.
+        reformatted_signed_request.get("headers").append(
+            {"key": "x-goog-cloud-target-resource", "value": AUDIENCE}
+        ),
+        return urllib.parse.quote(
+            json.dumps(
+                reformatted_signed_request, separators=(",", ":"), sort_keys=True
+            )
+        )
+
+    @classmethod
+    def make_mock_request(
+        cls,
+        region_status=None,
+        region_name=None,
+        role_status=None,
+        role_name=None,
+        security_credentials_status=None,
+        security_credentials_data=None,
+        token_status=None,
+        token_data=None,
+        impersonation_status=None,
+        impersonation_data=None,
+    ):
+        """Utility function to generate a mock HTTP request object.
+        This will facilitate testing various edge cases by specify how the
+        various endpoints will respond while generating a Google Access token
+        in an AWS environment.
+        """
+        responses = []
+        if region_status:
+            # AWS region request.
+            region_response = mock.create_autospec(transport.Response, instance=True)
+            region_response.status = region_status
+            if region_name:
+                region_response.data = "{}b".format(region_name).encode("utf-8")
+            responses.append(region_response)
+
+        if role_status:
+            # AWS role name request.
+            role_response = mock.create_autospec(transport.Response, instance=True)
+            role_response.status = role_status
+            if role_name:
+                role_response.data = role_name.encode("utf-8")
+            responses.append(role_response)
+
+        if security_credentials_status:
+            # AWS security credentials request.
+            security_credentials_response = mock.create_autospec(
+                transport.Response, instance=True
+            )
+            security_credentials_response.status = security_credentials_status
+            if security_credentials_data:
+                security_credentials_response.data = json.dumps(
+                    security_credentials_data
+                ).encode("utf-8")
+            responses.append(security_credentials_response)
+
+        if token_status:
+            # GCP token exchange request.
+            token_response = mock.create_autospec(transport.Response, instance=True)
+            token_response.status = token_status
+            token_response.data = json.dumps(token_data).encode("utf-8")
+            responses.append(token_response)
+
+        if impersonation_status:
+            # Service account impersonation request.
+            impersonation_response = mock.create_autospec(
+                transport.Response, instance=True
+            )
+            impersonation_response.status = impersonation_status
+            impersonation_response.data = json.dumps(impersonation_data).encode("utf-8")
+            responses.append(impersonation_response)
+
+        request = mock.create_autospec(transport.Request)
+        request.side_effect = responses
+
+        return request
+
+    @classmethod
+    def make_credentials(
+        cls,
+        credential_source,
+        client_id=None,
+        client_secret=None,
+        quota_project_id=None,
+        scopes=None,
+        default_scopes=None,
+        service_account_impersonation_url=None,
+    ):
+        return aws.Credentials(
+            audience=AUDIENCE,
+            subject_token_type=SUBJECT_TOKEN_TYPE,
+            token_url=TOKEN_URL,
+            service_account_impersonation_url=service_account_impersonation_url,
+            credential_source=credential_source,
+            client_id=client_id,
+            client_secret=client_secret,
+            quota_project_id=quota_project_id,
+            scopes=scopes,
+            default_scopes=default_scopes,
+        )
+
+    @classmethod
+    def assert_aws_metadata_request_kwargs(cls, request_kwargs, url, headers=None):
+        assert request_kwargs["url"] == url
+        # All used AWS metadata server endpoints use GET HTTP method.
+        assert request_kwargs["method"] == "GET"
+        if headers:
+            assert request_kwargs["headers"] == headers
+        else:
+            assert "headers" not in request_kwargs
+        # None of the endpoints used require any data in request.
+        assert "body" not in request_kwargs
+
+    @classmethod
+    def assert_token_request_kwargs(
+        cls, request_kwargs, headers, request_data, token_url=TOKEN_URL
+    ):
+        assert request_kwargs["url"] == token_url
+        assert request_kwargs["method"] == "POST"
+        assert request_kwargs["headers"] == headers
+        assert request_kwargs["body"] is not None
+        body_tuples = urllib.parse.parse_qsl(request_kwargs["body"])
+        assert len(body_tuples) == len(request_data.keys())
+        for (k, v) in body_tuples:
+            assert v.decode("utf-8") == request_data[k.decode("utf-8")]
+
+    @classmethod
+    def assert_impersonation_request_kwargs(
+        cls,
+        request_kwargs,
+        headers,
+        request_data,
+        service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+    ):
+        assert request_kwargs["url"] == service_account_impersonation_url
+        assert request_kwargs["method"] == "POST"
+        assert request_kwargs["headers"] == headers
+        assert request_kwargs["body"] is not None
+        body_json = json.loads(request_kwargs["body"].decode("utf-8"))
+        assert body_json == request_data
+
+    @mock.patch.object(aws.Credentials, "__init__", return_value=None)
+    def test_from_info_full_options(self, mock_init):
+        credentials = aws.Credentials.from_info(
+            {
+                "audience": AUDIENCE,
+                "subject_token_type": SUBJECT_TOKEN_TYPE,
+                "token_url": TOKEN_URL,
+                "service_account_impersonation_url": SERVICE_ACCOUNT_IMPERSONATION_URL,
+                "client_id": CLIENT_ID,
+                "client_secret": CLIENT_SECRET,
+                "quota_project_id": QUOTA_PROJECT_ID,
+                "credential_source": self.CREDENTIAL_SOURCE,
+            }
+        )
+
+        # Confirm aws.Credentials instance initialized with the expected parameters.
+        assert isinstance(credentials, aws.Credentials)
+        mock_init.assert_called_once_with(
+            audience=AUDIENCE,
+            subject_token_type=SUBJECT_TOKEN_TYPE,
+            token_url=TOKEN_URL,
+            service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+            client_id=CLIENT_ID,
+            client_secret=CLIENT_SECRET,
+            credential_source=self.CREDENTIAL_SOURCE,
+            quota_project_id=QUOTA_PROJECT_ID,
+        )
+
+    @mock.patch.object(aws.Credentials, "__init__", return_value=None)
+    def test_from_info_required_options_only(self, mock_init):
+        credentials = aws.Credentials.from_info(
+            {
+                "audience": AUDIENCE,
+                "subject_token_type": SUBJECT_TOKEN_TYPE,
+                "token_url": TOKEN_URL,
+                "credential_source": self.CREDENTIAL_SOURCE,
+            }
+        )
+
+        # Confirm aws.Credentials instance initialized with the expected parameters.
+        assert isinstance(credentials, aws.Credentials)
+        mock_init.assert_called_once_with(
+            audience=AUDIENCE,
+            subject_token_type=SUBJECT_TOKEN_TYPE,
+            token_url=TOKEN_URL,
+            service_account_impersonation_url=None,
+            client_id=None,
+            client_secret=None,
+            credential_source=self.CREDENTIAL_SOURCE,
+            quota_project_id=None,
+        )
+
+    @mock.patch.object(aws.Credentials, "__init__", return_value=None)
+    def test_from_file_full_options(self, mock_init, tmpdir):
+        info = {
+            "audience": AUDIENCE,
+            "subject_token_type": SUBJECT_TOKEN_TYPE,
+            "token_url": TOKEN_URL,
+            "service_account_impersonation_url": SERVICE_ACCOUNT_IMPERSONATION_URL,
+            "client_id": CLIENT_ID,
+            "client_secret": CLIENT_SECRET,
+            "quota_project_id": QUOTA_PROJECT_ID,
+            "credential_source": self.CREDENTIAL_SOURCE,
+        }
+        config_file = tmpdir.join("config.json")
+        config_file.write(json.dumps(info))
+        credentials = aws.Credentials.from_file(str(config_file))
+
+        # Confirm aws.Credentials instance initialized with the expected parameters.
+        assert isinstance(credentials, aws.Credentials)
+        mock_init.assert_called_once_with(
+            audience=AUDIENCE,
+            subject_token_type=SUBJECT_TOKEN_TYPE,
+            token_url=TOKEN_URL,
+            service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+            client_id=CLIENT_ID,
+            client_secret=CLIENT_SECRET,
+            credential_source=self.CREDENTIAL_SOURCE,
+            quota_project_id=QUOTA_PROJECT_ID,
+        )
+
+    @mock.patch.object(aws.Credentials, "__init__", return_value=None)
+    def test_from_file_required_options_only(self, mock_init, tmpdir):
+        info = {
+            "audience": AUDIENCE,
+            "subject_token_type": SUBJECT_TOKEN_TYPE,
+            "token_url": TOKEN_URL,
+            "credential_source": self.CREDENTIAL_SOURCE,
+        }
+        config_file = tmpdir.join("config.json")
+        config_file.write(json.dumps(info))
+        credentials = aws.Credentials.from_file(str(config_file))
+
+        # Confirm aws.Credentials instance initialized with the expected parameters.
+        assert isinstance(credentials, aws.Credentials)
+        mock_init.assert_called_once_with(
+            audience=AUDIENCE,
+            subject_token_type=SUBJECT_TOKEN_TYPE,
+            token_url=TOKEN_URL,
+            service_account_impersonation_url=None,
+            client_id=None,
+            client_secret=None,
+            credential_source=self.CREDENTIAL_SOURCE,
+            quota_project_id=None,
+        )
+
+    def test_constructor_invalid_credential_source(self):
+        # Provide invalid credential source.
+        credential_source = {"unsupported": "value"}
+
+        with pytest.raises(ValueError) as excinfo:
+            self.make_credentials(credential_source=credential_source)
+
+        assert excinfo.match(r"No valid AWS 'credential_source' provided")
+
+    def test_constructor_invalid_environment_id(self):
+        # Provide invalid environment_id.
+        credential_source = self.CREDENTIAL_SOURCE.copy()
+        credential_source["environment_id"] = "azure1"
+
+        with pytest.raises(ValueError) as excinfo:
+            self.make_credentials(credential_source=credential_source)
+
+        assert excinfo.match(r"No valid AWS 'credential_source' provided")
+
+    def test_constructor_missing_cred_verification_url(self):
+        # regional_cred_verification_url is a required field.
+        credential_source = self.CREDENTIAL_SOURCE.copy()
+        credential_source.pop("regional_cred_verification_url")
+
+        with pytest.raises(ValueError) as excinfo:
+            self.make_credentials(credential_source=credential_source)
+
+        assert excinfo.match(r"No valid AWS 'credential_source' provided")
+
+    def test_constructor_invalid_environment_id_version(self):
+        # Provide an unsupported version.
+        credential_source = self.CREDENTIAL_SOURCE.copy()
+        credential_source["environment_id"] = "aws3"
+
+        with pytest.raises(ValueError) as excinfo:
+            self.make_credentials(credential_source=credential_source)
+
+        assert excinfo.match(r"aws version '3' is not supported in the current build.")
+
+    def test_info(self):
+        credentials = self.make_credentials(
+            credential_source=self.CREDENTIAL_SOURCE.copy()
+        )
+
+        assert credentials.info == {
+            "type": "external_account",
+            "audience": AUDIENCE,
+            "subject_token_type": SUBJECT_TOKEN_TYPE,
+            "token_url": TOKEN_URL,
+            "credential_source": self.CREDENTIAL_SOURCE,
+        }
+
+    def test_retrieve_subject_token_missing_region_url(self):
+        # When AWS_REGION envvar is not available, region_url is required for
+        # determining the current AWS region.
+        credential_source = self.CREDENTIAL_SOURCE.copy()
+        credential_source.pop("region_url")
+        credentials = self.make_credentials(credential_source=credential_source)
+
+        with pytest.raises(exceptions.RefreshError) as excinfo:
+            credentials.retrieve_subject_token(None)
+
+        assert excinfo.match(r"Unable to determine AWS region")
+
+    @mock.patch("google.auth._helpers.utcnow")
+    def test_retrieve_subject_token_success_temp_creds_no_environment_vars(
+        self, utcnow
+    ):
+        utcnow.return_value = datetime.datetime.strptime(
+            self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
+        )
+        request = self.make_mock_request(
+            region_status=http_client.OK,
+            region_name=self.AWS_REGION,
+            role_status=http_client.OK,
+            role_name=self.AWS_ROLE,
+            security_credentials_status=http_client.OK,
+            security_credentials_data=self.AWS_SECURITY_CREDENTIALS_RESPONSE,
+        )
+        credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE)
+
+        subject_token = credentials.retrieve_subject_token(request)
+
+        assert subject_token == self.make_serialized_aws_signed_request(
+            {
+                "access_key_id": ACCESS_KEY_ID,
+                "secret_access_key": SECRET_ACCESS_KEY,
+                "security_token": TOKEN,
+            }
+        )
+        # Assert region request.
+        self.assert_aws_metadata_request_kwargs(
+            request.call_args_list[0][1], REGION_URL
+        )
+        # Assert role request.
+        self.assert_aws_metadata_request_kwargs(
+            request.call_args_list[1][1], SECURITY_CREDS_URL
+        )
+        # Assert security credentials request.
+        self.assert_aws_metadata_request_kwargs(
+            request.call_args_list[2][1],
+            "{}/{}".format(SECURITY_CREDS_URL, self.AWS_ROLE),
+            {"Content-Type": "application/json"},
+        )
+
+        # Retrieve subject_token again. Region should not be queried again.
+        new_request = self.make_mock_request(
+            role_status=http_client.OK,
+            role_name=self.AWS_ROLE,
+            security_credentials_status=http_client.OK,
+            security_credentials_data=self.AWS_SECURITY_CREDENTIALS_RESPONSE,
+        )
+
+        credentials.retrieve_subject_token(new_request)
+
+        # Only 2 requests should be sent as the region is cached.
+        assert len(new_request.call_args_list) == 2
+        # Assert role request.
+        self.assert_aws_metadata_request_kwargs(
+            new_request.call_args_list[0][1], SECURITY_CREDS_URL
+        )
+        # Assert security credentials request.
+        self.assert_aws_metadata_request_kwargs(
+            new_request.call_args_list[1][1],
+            "{}/{}".format(SECURITY_CREDS_URL, self.AWS_ROLE),
+            {"Content-Type": "application/json"},
+        )
+
+    @mock.patch("google.auth._helpers.utcnow")
+    def test_retrieve_subject_token_success_permanent_creds_no_environment_vars(
+        self, utcnow
+    ):
+        # Simualte a permanent credential without a session token is
+        # returned by the security-credentials endpoint.
+        security_creds_response = self.AWS_SECURITY_CREDENTIALS_RESPONSE.copy()
+        security_creds_response.pop("Token")
+        utcnow.return_value = datetime.datetime.strptime(
+            self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
+        )
+        request = self.make_mock_request(
+            region_status=http_client.OK,
+            region_name=self.AWS_REGION,
+            role_status=http_client.OK,
+            role_name=self.AWS_ROLE,
+            security_credentials_status=http_client.OK,
+            security_credentials_data=security_creds_response,
+        )
+        credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE)
+
+        subject_token = credentials.retrieve_subject_token(request)
+
+        assert subject_token == self.make_serialized_aws_signed_request(
+            {"access_key_id": ACCESS_KEY_ID, "secret_access_key": SECRET_ACCESS_KEY}
+        )
+
+    @mock.patch("google.auth._helpers.utcnow")
+    def test_retrieve_subject_token_success_environment_vars(self, utcnow, monkeypatch):
+        monkeypatch.setenv(environment_vars.AWS_ACCESS_KEY_ID, ACCESS_KEY_ID)
+        monkeypatch.setenv(environment_vars.AWS_SECRET_ACCESS_KEY, SECRET_ACCESS_KEY)
+        monkeypatch.setenv(environment_vars.AWS_SESSION_TOKEN, TOKEN)
+        monkeypatch.setenv(environment_vars.AWS_REGION, self.AWS_REGION)
+        utcnow.return_value = datetime.datetime.strptime(
+            self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
+        )
+        credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE)
+
+        subject_token = credentials.retrieve_subject_token(None)
+
+        assert subject_token == self.make_serialized_aws_signed_request(
+            {
+                "access_key_id": ACCESS_KEY_ID,
+                "secret_access_key": SECRET_ACCESS_KEY,
+                "security_token": TOKEN,
+            }
+        )
+
+    @mock.patch("google.auth._helpers.utcnow")
+    def test_retrieve_subject_token_success_environment_vars_with_default_region(
+        self, utcnow, monkeypatch
+    ):
+        monkeypatch.setenv(environment_vars.AWS_ACCESS_KEY_ID, ACCESS_KEY_ID)
+        monkeypatch.setenv(environment_vars.AWS_SECRET_ACCESS_KEY, SECRET_ACCESS_KEY)
+        monkeypatch.setenv(environment_vars.AWS_SESSION_TOKEN, TOKEN)
+        monkeypatch.setenv(environment_vars.AWS_DEFAULT_REGION, self.AWS_REGION)
+        utcnow.return_value = datetime.datetime.strptime(
+            self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
+        )
+        credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE)
+
+        subject_token = credentials.retrieve_subject_token(None)
+
+        assert subject_token == self.make_serialized_aws_signed_request(
+            {
+                "access_key_id": ACCESS_KEY_ID,
+                "secret_access_key": SECRET_ACCESS_KEY,
+                "security_token": TOKEN,
+            }
+        )
+
+    @mock.patch("google.auth._helpers.utcnow")
+    def test_retrieve_subject_token_success_environment_vars_with_both_regions_set(
+        self, utcnow, monkeypatch
+    ):
+        monkeypatch.setenv(environment_vars.AWS_ACCESS_KEY_ID, ACCESS_KEY_ID)
+        monkeypatch.setenv(environment_vars.AWS_SECRET_ACCESS_KEY, SECRET_ACCESS_KEY)
+        monkeypatch.setenv(environment_vars.AWS_SESSION_TOKEN, TOKEN)
+        monkeypatch.setenv(environment_vars.AWS_DEFAULT_REGION, "Malformed AWS Region")
+        # This test makes sure that the AWS_REGION gets used over AWS_DEFAULT_REGION,
+        # So, AWS_DEFAULT_REGION is set to something that would cause the test to fail,
+        # And AWS_REGION is set to the a valid value, and it should succeed
+        monkeypatch.setenv(environment_vars.AWS_REGION, self.AWS_REGION)
+        utcnow.return_value = datetime.datetime.strptime(
+            self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
+        )
+        credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE)
+
+        subject_token = credentials.retrieve_subject_token(None)
+
+        assert subject_token == self.make_serialized_aws_signed_request(
+            {
+                "access_key_id": ACCESS_KEY_ID,
+                "secret_access_key": SECRET_ACCESS_KEY,
+                "security_token": TOKEN,
+            }
+        )
+
+    @mock.patch("google.auth._helpers.utcnow")
+    def test_retrieve_subject_token_success_environment_vars_no_session_token(
+        self, utcnow, monkeypatch
+    ):
+        monkeypatch.setenv(environment_vars.AWS_ACCESS_KEY_ID, ACCESS_KEY_ID)
+        monkeypatch.setenv(environment_vars.AWS_SECRET_ACCESS_KEY, SECRET_ACCESS_KEY)
+        monkeypatch.setenv(environment_vars.AWS_REGION, self.AWS_REGION)
+        utcnow.return_value = datetime.datetime.strptime(
+            self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
+        )
+        credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE)
+
+        subject_token = credentials.retrieve_subject_token(None)
+
+        assert subject_token == self.make_serialized_aws_signed_request(
+            {"access_key_id": ACCESS_KEY_ID, "secret_access_key": SECRET_ACCESS_KEY}
+        )
+
+    @mock.patch("google.auth._helpers.utcnow")
+    def test_retrieve_subject_token_success_environment_vars_except_region(
+        self, utcnow, monkeypatch
+    ):
+        monkeypatch.setenv(environment_vars.AWS_ACCESS_KEY_ID, ACCESS_KEY_ID)
+        monkeypatch.setenv(environment_vars.AWS_SECRET_ACCESS_KEY, SECRET_ACCESS_KEY)
+        monkeypatch.setenv(environment_vars.AWS_SESSION_TOKEN, TOKEN)
+        utcnow.return_value = datetime.datetime.strptime(
+            self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
+        )
+        # Region will be queried since it is not found in envvars.
+        request = self.make_mock_request(
+            region_status=http_client.OK, region_name=self.AWS_REGION
+        )
+        credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE)
+
+        subject_token = credentials.retrieve_subject_token(request)
+
+        assert subject_token == self.make_serialized_aws_signed_request(
+            {
+                "access_key_id": ACCESS_KEY_ID,
+                "secret_access_key": SECRET_ACCESS_KEY,
+                "security_token": TOKEN,
+            }
+        )
+
+    def test_retrieve_subject_token_error_determining_aws_region(self):
+        # Simulate error in retrieving the AWS region.
+        request = self.make_mock_request(region_status=http_client.BAD_REQUEST)
+        credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE)
+
+        with pytest.raises(exceptions.RefreshError) as excinfo:
+            credentials.retrieve_subject_token(request)
+
+        assert excinfo.match(r"Unable to retrieve AWS region")
+
+    def test_retrieve_subject_token_error_determining_aws_role(self):
+        # Simulate error in retrieving the AWS role name.
+        request = self.make_mock_request(
+            region_status=http_client.OK,
+            region_name=self.AWS_REGION,
+            role_status=http_client.BAD_REQUEST,
+        )
+        credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE)
+
+        with pytest.raises(exceptions.RefreshError) as excinfo:
+            credentials.retrieve_subject_token(request)
+
+        assert excinfo.match(r"Unable to retrieve AWS role name")
+
+    def test_retrieve_subject_token_error_determining_security_creds_url(self):
+        # Simulate the security-credentials url is missing. This is needed for
+        # determining the AWS security credentials when not found in envvars.
+        credential_source = self.CREDENTIAL_SOURCE.copy()
+        credential_source.pop("url")
+        request = self.make_mock_request(
+            region_status=http_client.OK, region_name=self.AWS_REGION
+        )
+        credentials = self.make_credentials(credential_source=credential_source)
+
+        with pytest.raises(exceptions.RefreshError) as excinfo:
+            credentials.retrieve_subject_token(request)
+
+        assert excinfo.match(
+            r"Unable to determine the AWS metadata server security credentials endpoint"
+        )
+
+    def test_retrieve_subject_token_error_determining_aws_security_creds(self):
+        # Simulate error in retrieving the AWS security credentials.
+        request = self.make_mock_request(
+            region_status=http_client.OK,
+            region_name=self.AWS_REGION,
+            role_status=http_client.OK,
+            role_name=self.AWS_ROLE,
+            security_credentials_status=http_client.BAD_REQUEST,
+        )
+        credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE)
+
+        with pytest.raises(exceptions.RefreshError) as excinfo:
+            credentials.retrieve_subject_token(request)
+
+        assert excinfo.match(r"Unable to retrieve AWS security credentials")
+
+    @mock.patch("google.auth._helpers.utcnow")
+    def test_refresh_success_without_impersonation_ignore_default_scopes(self, utcnow):
+        utcnow.return_value = datetime.datetime.strptime(
+            self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
+        )
+        expected_subject_token = self.make_serialized_aws_signed_request(
+            {
+                "access_key_id": ACCESS_KEY_ID,
+                "secret_access_key": SECRET_ACCESS_KEY,
+                "security_token": TOKEN,
+            }
+        )
+        token_headers = {
+            "Content-Type": "application/x-www-form-urlencoded",
+            "Authorization": "Basic " + BASIC_AUTH_ENCODING,
+        }
+        token_request_data = {
+            "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
+            "audience": AUDIENCE,
+            "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
+            "scope": " ".join(SCOPES),
+            "subject_token": expected_subject_token,
+            "subject_token_type": SUBJECT_TOKEN_TYPE,
+        }
+        request = self.make_mock_request(
+            region_status=http_client.OK,
+            region_name=self.AWS_REGION,
+            role_status=http_client.OK,
+            role_name=self.AWS_ROLE,
+            security_credentials_status=http_client.OK,
+            security_credentials_data=self.AWS_SECURITY_CREDENTIALS_RESPONSE,
+            token_status=http_client.OK,
+            token_data=self.SUCCESS_RESPONSE,
+        )
+        credentials = self.make_credentials(
+            client_id=CLIENT_ID,
+            client_secret=CLIENT_SECRET,
+            credential_source=self.CREDENTIAL_SOURCE,
+            quota_project_id=QUOTA_PROJECT_ID,
+            scopes=SCOPES,
+            # Default scopes should be ignored.
+            default_scopes=["ignored"],
+        )
+
+        credentials.refresh(request)
+
+        assert len(request.call_args_list) == 4
+        # Fourth request should be sent to GCP STS endpoint.
+        self.assert_token_request_kwargs(
+            request.call_args_list[3][1], token_headers, token_request_data
+        )
+        assert credentials.token == self.SUCCESS_RESPONSE["access_token"]
+        assert credentials.quota_project_id == QUOTA_PROJECT_ID
+        assert credentials.scopes == SCOPES
+        assert credentials.default_scopes == ["ignored"]
+
+    @mock.patch("google.auth._helpers.utcnow")
+    def test_refresh_success_without_impersonation_use_default_scopes(self, utcnow):
+        utcnow.return_value = datetime.datetime.strptime(
+            self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
+        )
+        expected_subject_token = self.make_serialized_aws_signed_request(
+            {
+                "access_key_id": ACCESS_KEY_ID,
+                "secret_access_key": SECRET_ACCESS_KEY,
+                "security_token": TOKEN,
+            }
+        )
+        token_headers = {
+            "Content-Type": "application/x-www-form-urlencoded",
+            "Authorization": "Basic " + BASIC_AUTH_ENCODING,
+        }
+        token_request_data = {
+            "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
+            "audience": AUDIENCE,
+            "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
+            "scope": " ".join(SCOPES),
+            "subject_token": expected_subject_token,
+            "subject_token_type": SUBJECT_TOKEN_TYPE,
+        }
+        request = self.make_mock_request(
+            region_status=http_client.OK,
+            region_name=self.AWS_REGION,
+            role_status=http_client.OK,
+            role_name=self.AWS_ROLE,
+            security_credentials_status=http_client.OK,
+            security_credentials_data=self.AWS_SECURITY_CREDENTIALS_RESPONSE,
+            token_status=http_client.OK,
+            token_data=self.SUCCESS_RESPONSE,
+        )
+        credentials = self.make_credentials(
+            client_id=CLIENT_ID,
+            client_secret=CLIENT_SECRET,
+            credential_source=self.CREDENTIAL_SOURCE,
+            quota_project_id=QUOTA_PROJECT_ID,
+            scopes=None,
+            # Default scopes should be used since user specified scopes are none.
+            default_scopes=SCOPES,
+        )
+
+        credentials.refresh(request)
+
+        assert len(request.call_args_list) == 4
+        # Fourth request should be sent to GCP STS endpoint.
+        self.assert_token_request_kwargs(
+            request.call_args_list[3][1], token_headers, token_request_data
+        )
+        assert credentials.token == self.SUCCESS_RESPONSE["access_token"]
+        assert credentials.quota_project_id == QUOTA_PROJECT_ID
+        assert credentials.scopes is None
+        assert credentials.default_scopes == SCOPES
+
+    @mock.patch("google.auth._helpers.utcnow")
+    def test_refresh_success_with_impersonation_ignore_default_scopes(self, utcnow):
+        utcnow.return_value = datetime.datetime.strptime(
+            self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
+        )
+        expire_time = (
+            _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=3600)
+        ).isoformat("T") + "Z"
+        expected_subject_token = self.make_serialized_aws_signed_request(
+            {
+                "access_key_id": ACCESS_KEY_ID,
+                "secret_access_key": SECRET_ACCESS_KEY,
+                "security_token": TOKEN,
+            }
+        )
+        token_headers = {
+            "Content-Type": "application/x-www-form-urlencoded",
+            "Authorization": "Basic " + BASIC_AUTH_ENCODING,
+        }
+        token_request_data = {
+            "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
+            "audience": AUDIENCE,
+            "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
+            "scope": "https://www.googleapis.com/auth/iam",
+            "subject_token": expected_subject_token,
+            "subject_token_type": SUBJECT_TOKEN_TYPE,
+        }
+        # Service account impersonation request/response.
+        impersonation_response = {
+            "accessToken": "SA_ACCESS_TOKEN",
+            "expireTime": expire_time,
+        }
+        impersonation_headers = {
+            "Content-Type": "application/json",
+            "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]),
+            "x-goog-user-project": QUOTA_PROJECT_ID,
+        }
+        impersonation_request_data = {
+            "delegates": None,
+            "scope": SCOPES,
+            "lifetime": "3600s",
+        }
+        request = self.make_mock_request(
+            region_status=http_client.OK,
+            region_name=self.AWS_REGION,
+            role_status=http_client.OK,
+            role_name=self.AWS_ROLE,
+            security_credentials_status=http_client.OK,
+            security_credentials_data=self.AWS_SECURITY_CREDENTIALS_RESPONSE,
+            token_status=http_client.OK,
+            token_data=self.SUCCESS_RESPONSE,
+            impersonation_status=http_client.OK,
+            impersonation_data=impersonation_response,
+        )
+        credentials = self.make_credentials(
+            client_id=CLIENT_ID,
+            client_secret=CLIENT_SECRET,
+            credential_source=self.CREDENTIAL_SOURCE,
+            service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+            quota_project_id=QUOTA_PROJECT_ID,
+            scopes=SCOPES,
+            # Default scopes should be ignored.
+            default_scopes=["ignored"],
+        )
+
+        credentials.refresh(request)
+
+        assert len(request.call_args_list) == 5
+        # Fourth request should be sent to GCP STS endpoint.
+        self.assert_token_request_kwargs(
+            request.call_args_list[3][1], token_headers, token_request_data
+        )
+        # Fifth request should be sent to iamcredentials endpoint for service
+        # account impersonation.
+        self.assert_impersonation_request_kwargs(
+            request.call_args_list[4][1],
+            impersonation_headers,
+            impersonation_request_data,
+        )
+        assert credentials.token == impersonation_response["accessToken"]
+        assert credentials.quota_project_id == QUOTA_PROJECT_ID
+        assert credentials.scopes == SCOPES
+        assert credentials.default_scopes == ["ignored"]
+
+    @mock.patch("google.auth._helpers.utcnow")
+    def test_refresh_success_with_impersonation_use_default_scopes(self, utcnow):
+        utcnow.return_value = datetime.datetime.strptime(
+            self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
+        )
+        expire_time = (
+            _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=3600)
+        ).isoformat("T") + "Z"
+        expected_subject_token = self.make_serialized_aws_signed_request(
+            {
+                "access_key_id": ACCESS_KEY_ID,
+                "secret_access_key": SECRET_ACCESS_KEY,
+                "security_token": TOKEN,
+            }
+        )
+        token_headers = {
+            "Content-Type": "application/x-www-form-urlencoded",
+            "Authorization": "Basic " + BASIC_AUTH_ENCODING,
+        }
+        token_request_data = {
+            "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
+            "audience": AUDIENCE,
+            "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
+            "scope": "https://www.googleapis.com/auth/iam",
+            "subject_token": expected_subject_token,
+            "subject_token_type": SUBJECT_TOKEN_TYPE,
+        }
+        # Service account impersonation request/response.
+        impersonation_response = {
+            "accessToken": "SA_ACCESS_TOKEN",
+            "expireTime": expire_time,
+        }
+        impersonation_headers = {
+            "Content-Type": "application/json",
+            "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]),
+            "x-goog-user-project": QUOTA_PROJECT_ID,
+        }
+        impersonation_request_data = {
+            "delegates": None,
+            "scope": SCOPES,
+            "lifetime": "3600s",
+        }
+        request = self.make_mock_request(
+            region_status=http_client.OK,
+            region_name=self.AWS_REGION,
+            role_status=http_client.OK,
+            role_name=self.AWS_ROLE,
+            security_credentials_status=http_client.OK,
+            security_credentials_data=self.AWS_SECURITY_CREDENTIALS_RESPONSE,
+            token_status=http_client.OK,
+            token_data=self.SUCCESS_RESPONSE,
+            impersonation_status=http_client.OK,
+            impersonation_data=impersonation_response,
+        )
+        credentials = self.make_credentials(
+            client_id=CLIENT_ID,
+            client_secret=CLIENT_SECRET,
+            credential_source=self.CREDENTIAL_SOURCE,
+            service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+            quota_project_id=QUOTA_PROJECT_ID,
+            scopes=None,
+            # Default scopes should be used since user specified scopes are none.
+            default_scopes=SCOPES,
+        )
+
+        credentials.refresh(request)
+
+        assert len(request.call_args_list) == 5
+        # Fourth request should be sent to GCP STS endpoint.
+        self.assert_token_request_kwargs(
+            request.call_args_list[3][1], token_headers, token_request_data
+        )
+        # Fifth request should be sent to iamcredentials endpoint for service
+        # account impersonation.
+        self.assert_impersonation_request_kwargs(
+            request.call_args_list[4][1],
+            impersonation_headers,
+            impersonation_request_data,
+        )
+        assert credentials.token == impersonation_response["accessToken"]
+        assert credentials.quota_project_id == QUOTA_PROJECT_ID
+        assert credentials.scopes is None
+        assert credentials.default_scopes == SCOPES
+
+    def test_refresh_with_retrieve_subject_token_error(self):
+        request = self.make_mock_request(region_status=http_client.BAD_REQUEST)
+        credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE)
+
+        with pytest.raises(exceptions.RefreshError) as excinfo:
+            credentials.refresh(request)
+
+        assert excinfo.match(r"Unable to retrieve AWS region")
diff --git a/tests/test_credentials.py b/tests/test_credentials.py
new file mode 100644
index 0000000..2de6388
--- /dev/null
+++ b/tests/test_credentials.py
@@ -0,0 +1,179 @@
+# Copyright 2016 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 datetime
+
+import pytest
+
+from google.auth import _helpers
+from google.auth import credentials
+
+
+class CredentialsImpl(credentials.Credentials):
+    def refresh(self, request):
+        self.token = request
+
+    def with_quota_project(self, quota_project_id):
+        raise NotImplementedError()
+
+
+def test_credentials_constructor():
+    credentials = CredentialsImpl()
+    assert not credentials.token
+    assert not credentials.expiry
+    assert not credentials.expired
+    assert not credentials.valid
+
+
+def test_expired_and_valid():
+    credentials = CredentialsImpl()
+    credentials.token = "token"
+
+    assert credentials.valid
+    assert not credentials.expired
+
+    # Set the expiration to one second more than now plus the clock skew
+    # accomodation. These credentials should be valid.
+    credentials.expiry = (
+        datetime.datetime.utcnow()
+        + _helpers.REFRESH_THRESHOLD
+        + datetime.timedelta(seconds=1)
+    )
+
+    assert credentials.valid
+    assert not credentials.expired
+
+    # Set the credentials expiration to now. Because of the clock skew
+    # accomodation, these credentials should report as expired.
+    credentials.expiry = datetime.datetime.utcnow()
+
+    assert not credentials.valid
+    assert credentials.expired
+
+
+def test_before_request():
+    credentials = CredentialsImpl()
+    request = "token"
+    headers = {}
+
+    # First call should call refresh, setting the token.
+    credentials.before_request(request, "http://example.com", "GET", headers)
+    assert credentials.valid
+    assert credentials.token == "token"
+    assert headers["authorization"] == "Bearer token"
+
+    request = "token2"
+    headers = {}
+
+    # Second call shouldn't call refresh.
+    credentials.before_request(request, "http://example.com", "GET", headers)
+    assert credentials.valid
+    assert credentials.token == "token"
+    assert headers["authorization"] == "Bearer token"
+
+
+def test_anonymous_credentials_ctor():
+    anon = credentials.AnonymousCredentials()
+    assert anon.token is None
+    assert anon.expiry is None
+    assert not anon.expired
+    assert anon.valid
+
+
+def test_anonymous_credentials_refresh():
+    anon = credentials.AnonymousCredentials()
+    request = object()
+    with pytest.raises(ValueError):
+        anon.refresh(request)
+
+
+def test_anonymous_credentials_apply_default():
+    anon = credentials.AnonymousCredentials()
+    headers = {}
+    anon.apply(headers)
+    assert headers == {}
+    with pytest.raises(ValueError):
+        anon.apply(headers, token="TOKEN")
+
+
+def test_anonymous_credentials_before_request():
+    anon = credentials.AnonymousCredentials()
+    request = object()
+    method = "GET"
+    url = "https://example.com/api/endpoint"
+    headers = {}
+    anon.before_request(request, method, url, headers)
+    assert headers == {}
+
+
+class ReadOnlyScopedCredentialsImpl(credentials.ReadOnlyScoped, CredentialsImpl):
+    @property
+    def requires_scopes(self):
+        return super(ReadOnlyScopedCredentialsImpl, self).requires_scopes
+
+
+def test_readonly_scoped_credentials_constructor():
+    credentials = ReadOnlyScopedCredentialsImpl()
+    assert credentials._scopes is None
+
+
+def test_readonly_scoped_credentials_scopes():
+    credentials = ReadOnlyScopedCredentialsImpl()
+    credentials._scopes = ["one", "two"]
+    assert credentials.scopes == ["one", "two"]
+    assert credentials.has_scopes(["one"])
+    assert credentials.has_scopes(["two"])
+    assert credentials.has_scopes(["one", "two"])
+    assert not credentials.has_scopes(["three"])
+
+
+def test_readonly_scoped_credentials_requires_scopes():
+    credentials = ReadOnlyScopedCredentialsImpl()
+    assert not credentials.requires_scopes
+
+
+class RequiresScopedCredentialsImpl(credentials.Scoped, CredentialsImpl):
+    def __init__(self, scopes=None, default_scopes=None):
+        super(RequiresScopedCredentialsImpl, self).__init__()
+        self._scopes = scopes
+        self._default_scopes = default_scopes
+
+    @property
+    def requires_scopes(self):
+        return not self.scopes
+
+    def with_scopes(self, scopes, default_scopes=None):
+        return RequiresScopedCredentialsImpl(
+            scopes=scopes, default_scopes=default_scopes
+        )
+
+
+def test_create_scoped_if_required_scoped():
+    unscoped_credentials = RequiresScopedCredentialsImpl()
+    scoped_credentials = credentials.with_scopes_if_required(
+        unscoped_credentials, ["one", "two"]
+    )
+
+    assert scoped_credentials is not unscoped_credentials
+    assert not scoped_credentials.requires_scopes
+    assert scoped_credentials.has_scopes(["one", "two"])
+
+
+def test_create_scoped_if_required_not_scopes():
+    unscoped_credentials = CredentialsImpl()
+    scoped_credentials = credentials.with_scopes_if_required(
+        unscoped_credentials, ["one", "two"]
+    )
+
+    assert scoped_credentials is unscoped_credentials
diff --git a/tests/test_downscoped.py b/tests/test_downscoped.py
new file mode 100644
index 0000000..9ca95f5
--- /dev/null
+++ b/tests/test_downscoped.py
@@ -0,0 +1,696 @@
+# 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 datetime
+import json
+
+import mock
+import pytest
+from six.moves import http_client
+from six.moves import urllib
+
+from google.auth import _helpers
+from google.auth import credentials
+from google.auth import downscoped
+from google.auth import exceptions
+from google.auth import transport
+
+
+EXPRESSION = (
+    "resource.name.startsWith('projects/_/buckets/example-bucket/objects/customer-a')"
+)
+TITLE = "customer-a-objects"
+DESCRIPTION = (
+    "Condition to make permissions available for objects starting with customer-a"
+)
+AVAILABLE_RESOURCE = "//storage.googleapis.com/projects/_/buckets/example-bucket"
+AVAILABLE_PERMISSIONS = ["inRole:roles/storage.objectViewer"]
+
+OTHER_EXPRESSION = (
+    "resource.name.startsWith('projects/_/buckets/example-bucket/objects/customer-b')"
+)
+OTHER_TITLE = "customer-b-objects"
+OTHER_DESCRIPTION = (
+    "Condition to make permissions available for objects starting with customer-b"
+)
+OTHER_AVAILABLE_RESOURCE = "//storage.googleapis.com/projects/_/buckets/other-bucket"
+OTHER_AVAILABLE_PERMISSIONS = ["inRole:roles/storage.objectCreator"]
+QUOTA_PROJECT_ID = "QUOTA_PROJECT_ID"
+GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange"
+REQUESTED_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token"
+TOKEN_EXCHANGE_ENDPOINT = "https://sts.googleapis.com/v1/token"
+SUBJECT_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token"
+SUCCESS_RESPONSE = {
+    "access_token": "ACCESS_TOKEN",
+    "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
+    "token_type": "Bearer",
+    "expires_in": 3600,
+}
+ERROR_RESPONSE = {
+    "error": "invalid_grant",
+    "error_description": "Subject token is invalid.",
+    "error_uri": "https://tools.ietf.org/html/rfc6749",
+}
+CREDENTIAL_ACCESS_BOUNDARY_JSON = {
+    "accessBoundary": {
+        "accessBoundaryRules": [
+            {
+                "availablePermissions": AVAILABLE_PERMISSIONS,
+                "availableResource": AVAILABLE_RESOURCE,
+                "availabilityCondition": {
+                    "expression": EXPRESSION,
+                    "title": TITLE,
+                    "description": DESCRIPTION,
+                },
+            }
+        ]
+    }
+}
+
+
+class SourceCredentials(credentials.Credentials):
+    def __init__(self, raise_error=False, expires_in=3600):
+        super(SourceCredentials, self).__init__()
+        self._counter = 0
+        self._raise_error = raise_error
+        self._expires_in = expires_in
+
+    def refresh(self, request):
+        if self._raise_error:
+            raise exceptions.RefreshError(
+                "Failed to refresh access token in source credentials."
+            )
+        now = _helpers.utcnow()
+        self._counter += 1
+        self.token = "ACCESS_TOKEN_{}".format(self._counter)
+        self.expiry = now + datetime.timedelta(seconds=self._expires_in)
+
+
+def make_availability_condition(expression, title=None, description=None):
+    return downscoped.AvailabilityCondition(expression, title, description)
+
+
+def make_access_boundary_rule(
+    available_resource, available_permissions, availability_condition=None
+):
+    return downscoped.AccessBoundaryRule(
+        available_resource, available_permissions, availability_condition
+    )
+
+
+def make_credential_access_boundary(rules):
+    return downscoped.CredentialAccessBoundary(rules)
+
+
+class TestAvailabilityCondition(object):
+    def test_constructor(self):
+        availability_condition = make_availability_condition(
+            EXPRESSION, TITLE, DESCRIPTION
+        )
+
+        assert availability_condition.expression == EXPRESSION
+        assert availability_condition.title == TITLE
+        assert availability_condition.description == DESCRIPTION
+
+    def test_constructor_required_params_only(self):
+        availability_condition = make_availability_condition(EXPRESSION)
+
+        assert availability_condition.expression == EXPRESSION
+        assert availability_condition.title is None
+        assert availability_condition.description is None
+
+    def test_setters(self):
+        availability_condition = make_availability_condition(
+            EXPRESSION, TITLE, DESCRIPTION
+        )
+        availability_condition.expression = OTHER_EXPRESSION
+        availability_condition.title = OTHER_TITLE
+        availability_condition.description = OTHER_DESCRIPTION
+
+        assert availability_condition.expression == OTHER_EXPRESSION
+        assert availability_condition.title == OTHER_TITLE
+        assert availability_condition.description == OTHER_DESCRIPTION
+
+    def test_invalid_expression_type(self):
+        with pytest.raises(TypeError) as excinfo:
+            make_availability_condition([EXPRESSION], TITLE, DESCRIPTION)
+
+        assert excinfo.match("The provided expression is not a string.")
+
+    def test_invalid_title_type(self):
+        with pytest.raises(TypeError) as excinfo:
+            make_availability_condition(EXPRESSION, False, DESCRIPTION)
+
+        assert excinfo.match("The provided title is not a string or None.")
+
+    def test_invalid_description_type(self):
+        with pytest.raises(TypeError) as excinfo:
+            make_availability_condition(EXPRESSION, TITLE, False)
+
+        assert excinfo.match("The provided description is not a string or None.")
+
+    def test_to_json_required_params_only(self):
+        availability_condition = make_availability_condition(EXPRESSION)
+
+        assert availability_condition.to_json() == {"expression": EXPRESSION}
+
+    def test_to_json_(self):
+        availability_condition = make_availability_condition(
+            EXPRESSION, TITLE, DESCRIPTION
+        )
+
+        assert availability_condition.to_json() == {
+            "expression": EXPRESSION,
+            "title": TITLE,
+            "description": DESCRIPTION,
+        }
+
+
+class TestAccessBoundaryRule(object):
+    def test_constructor(self):
+        availability_condition = make_availability_condition(
+            EXPRESSION, TITLE, DESCRIPTION
+        )
+        access_boundary_rule = make_access_boundary_rule(
+            AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition
+        )
+
+        assert access_boundary_rule.available_resource == AVAILABLE_RESOURCE
+        assert access_boundary_rule.available_permissions == tuple(
+            AVAILABLE_PERMISSIONS
+        )
+        assert access_boundary_rule.availability_condition == availability_condition
+
+    def test_constructor_required_params_only(self):
+        access_boundary_rule = make_access_boundary_rule(
+            AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS
+        )
+
+        assert access_boundary_rule.available_resource == AVAILABLE_RESOURCE
+        assert access_boundary_rule.available_permissions == tuple(
+            AVAILABLE_PERMISSIONS
+        )
+        assert access_boundary_rule.availability_condition is None
+
+    def test_setters(self):
+        availability_condition = make_availability_condition(
+            EXPRESSION, TITLE, DESCRIPTION
+        )
+        other_availability_condition = make_availability_condition(
+            OTHER_EXPRESSION, OTHER_TITLE, OTHER_DESCRIPTION
+        )
+        access_boundary_rule = make_access_boundary_rule(
+            AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition
+        )
+        access_boundary_rule.available_resource = OTHER_AVAILABLE_RESOURCE
+        access_boundary_rule.available_permissions = OTHER_AVAILABLE_PERMISSIONS
+        access_boundary_rule.availability_condition = other_availability_condition
+
+        assert access_boundary_rule.available_resource == OTHER_AVAILABLE_RESOURCE
+        assert access_boundary_rule.available_permissions == tuple(
+            OTHER_AVAILABLE_PERMISSIONS
+        )
+        assert (
+            access_boundary_rule.availability_condition == other_availability_condition
+        )
+
+    def test_invalid_available_resource_type(self):
+        availability_condition = make_availability_condition(
+            EXPRESSION, TITLE, DESCRIPTION
+        )
+        with pytest.raises(TypeError) as excinfo:
+            make_access_boundary_rule(
+                None, AVAILABLE_PERMISSIONS, availability_condition
+            )
+
+        assert excinfo.match("The provided available_resource is not a string.")
+
+    def test_invalid_available_permissions_type(self):
+        availability_condition = make_availability_condition(
+            EXPRESSION, TITLE, DESCRIPTION
+        )
+        with pytest.raises(TypeError) as excinfo:
+            make_access_boundary_rule(
+                AVAILABLE_RESOURCE, [0, 1, 2], availability_condition
+            )
+
+        assert excinfo.match(
+            "Provided available_permissions are not a list of strings."
+        )
+
+    def test_invalid_available_permissions_value(self):
+        availability_condition = make_availability_condition(
+            EXPRESSION, TITLE, DESCRIPTION
+        )
+        with pytest.raises(ValueError) as excinfo:
+            make_access_boundary_rule(
+                AVAILABLE_RESOURCE,
+                ["roles/storage.objectViewer"],
+                availability_condition,
+            )
+
+        assert excinfo.match("available_permissions must be prefixed with 'inRole:'.")
+
+    def test_invalid_availability_condition_type(self):
+        with pytest.raises(TypeError) as excinfo:
+            make_access_boundary_rule(
+                AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, {"foo": "bar"}
+            )
+
+        assert excinfo.match(
+            "The provided availability_condition is not a 'google.auth.downscoped.AvailabilityCondition' or None."
+        )
+
+    def test_to_json(self):
+        availability_condition = make_availability_condition(
+            EXPRESSION, TITLE, DESCRIPTION
+        )
+        access_boundary_rule = make_access_boundary_rule(
+            AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition
+        )
+
+        assert access_boundary_rule.to_json() == {
+            "availablePermissions": AVAILABLE_PERMISSIONS,
+            "availableResource": AVAILABLE_RESOURCE,
+            "availabilityCondition": {
+                "expression": EXPRESSION,
+                "title": TITLE,
+                "description": DESCRIPTION,
+            },
+        }
+
+    def test_to_json_required_params_only(self):
+        access_boundary_rule = make_access_boundary_rule(
+            AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS
+        )
+
+        assert access_boundary_rule.to_json() == {
+            "availablePermissions": AVAILABLE_PERMISSIONS,
+            "availableResource": AVAILABLE_RESOURCE,
+        }
+
+
+class TestCredentialAccessBoundary(object):
+    def test_constructor(self):
+        availability_condition = make_availability_condition(
+            EXPRESSION, TITLE, DESCRIPTION
+        )
+        access_boundary_rule = make_access_boundary_rule(
+            AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition
+        )
+        rules = [access_boundary_rule]
+        credential_access_boundary = make_credential_access_boundary(rules)
+
+        assert credential_access_boundary.rules == tuple(rules)
+
+    def test_setters(self):
+        availability_condition = make_availability_condition(
+            EXPRESSION, TITLE, DESCRIPTION
+        )
+        access_boundary_rule = make_access_boundary_rule(
+            AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition
+        )
+        rules = [access_boundary_rule]
+        other_availability_condition = make_availability_condition(
+            OTHER_EXPRESSION, OTHER_TITLE, OTHER_DESCRIPTION
+        )
+        other_access_boundary_rule = make_access_boundary_rule(
+            OTHER_AVAILABLE_RESOURCE,
+            OTHER_AVAILABLE_PERMISSIONS,
+            other_availability_condition,
+        )
+        other_rules = [other_access_boundary_rule]
+        credential_access_boundary = make_credential_access_boundary(rules)
+        credential_access_boundary.rules = other_rules
+
+        assert credential_access_boundary.rules == tuple(other_rules)
+
+    def test_add_rule(self):
+        availability_condition = make_availability_condition(
+            EXPRESSION, TITLE, DESCRIPTION
+        )
+        access_boundary_rule = make_access_boundary_rule(
+            AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition
+        )
+        rules = [access_boundary_rule] * 9
+        credential_access_boundary = make_credential_access_boundary(rules)
+
+        # Add one more rule. This should not raise an error.
+        additional_access_boundary_rule = make_access_boundary_rule(
+            OTHER_AVAILABLE_RESOURCE, OTHER_AVAILABLE_PERMISSIONS
+        )
+        credential_access_boundary.add_rule(additional_access_boundary_rule)
+
+        assert len(credential_access_boundary.rules) == 10
+        assert credential_access_boundary.rules[9] == additional_access_boundary_rule
+
+    def test_add_rule_invalid_value(self):
+        availability_condition = make_availability_condition(
+            EXPRESSION, TITLE, DESCRIPTION
+        )
+        access_boundary_rule = make_access_boundary_rule(
+            AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition
+        )
+        rules = [access_boundary_rule] * 10
+        credential_access_boundary = make_credential_access_boundary(rules)
+
+        # Add one more rule to exceed maximum allowed rules.
+        with pytest.raises(ValueError) as excinfo:
+            credential_access_boundary.add_rule(access_boundary_rule)
+
+        assert excinfo.match(
+            "Credential access boundary rules can have a maximum of 10 rules."
+        )
+        assert len(credential_access_boundary.rules) == 10
+
+    def test_add_rule_invalid_type(self):
+        availability_condition = make_availability_condition(
+            EXPRESSION, TITLE, DESCRIPTION
+        )
+        access_boundary_rule = make_access_boundary_rule(
+            AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition
+        )
+        rules = [access_boundary_rule]
+        credential_access_boundary = make_credential_access_boundary(rules)
+
+        # Add an invalid rule to exceed maximum allowed rules.
+        with pytest.raises(TypeError) as excinfo:
+            credential_access_boundary.add_rule("invalid")
+
+        assert excinfo.match(
+            "The provided rule does not contain a valid 'google.auth.downscoped.AccessBoundaryRule'."
+        )
+        assert len(credential_access_boundary.rules) == 1
+        assert credential_access_boundary.rules[0] == access_boundary_rule
+
+    def test_invalid_rules_type(self):
+        with pytest.raises(TypeError) as excinfo:
+            make_credential_access_boundary(["invalid"])
+
+        assert excinfo.match(
+            "List of rules provided do not contain a valid 'google.auth.downscoped.AccessBoundaryRule'."
+        )
+
+    def test_invalid_rules_value(self):
+        availability_condition = make_availability_condition(
+            EXPRESSION, TITLE, DESCRIPTION
+        )
+        access_boundary_rule = make_access_boundary_rule(
+            AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition
+        )
+        too_many_rules = [access_boundary_rule] * 11
+        with pytest.raises(ValueError) as excinfo:
+            make_credential_access_boundary(too_many_rules)
+
+        assert excinfo.match(
+            "Credential access boundary rules can have a maximum of 10 rules."
+        )
+
+    def test_to_json(self):
+        availability_condition = make_availability_condition(
+            EXPRESSION, TITLE, DESCRIPTION
+        )
+        access_boundary_rule = make_access_boundary_rule(
+            AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition
+        )
+        rules = [access_boundary_rule]
+        credential_access_boundary = make_credential_access_boundary(rules)
+
+        assert credential_access_boundary.to_json() == {
+            "accessBoundary": {
+                "accessBoundaryRules": [
+                    {
+                        "availablePermissions": AVAILABLE_PERMISSIONS,
+                        "availableResource": AVAILABLE_RESOURCE,
+                        "availabilityCondition": {
+                            "expression": EXPRESSION,
+                            "title": TITLE,
+                            "description": DESCRIPTION,
+                        },
+                    }
+                ]
+            }
+        }
+
+
+class TestCredentials(object):
+    @staticmethod
+    def make_credentials(source_credentials=SourceCredentials(), quota_project_id=None):
+        availability_condition = make_availability_condition(
+            EXPRESSION, TITLE, DESCRIPTION
+        )
+        access_boundary_rule = make_access_boundary_rule(
+            AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition
+        )
+        rules = [access_boundary_rule]
+        credential_access_boundary = make_credential_access_boundary(rules)
+
+        return downscoped.Credentials(
+            source_credentials, credential_access_boundary, quota_project_id
+        )
+
+    @staticmethod
+    def make_mock_request(data, status=http_client.OK):
+        response = mock.create_autospec(transport.Response, instance=True)
+        response.status = status
+        response.data = json.dumps(data).encode("utf-8")
+
+        request = mock.create_autospec(transport.Request)
+        request.return_value = response
+
+        return request
+
+    @staticmethod
+    def assert_request_kwargs(request_kwargs, headers, request_data):
+        """Asserts the request was called with the expected parameters.
+        """
+        assert request_kwargs["url"] == TOKEN_EXCHANGE_ENDPOINT
+        assert request_kwargs["method"] == "POST"
+        assert request_kwargs["headers"] == headers
+        assert request_kwargs["body"] is not None
+        body_tuples = urllib.parse.parse_qsl(request_kwargs["body"])
+        for (k, v) in body_tuples:
+            assert v.decode("utf-8") == request_data[k.decode("utf-8")]
+        assert len(body_tuples) == len(request_data.keys())
+
+    def test_default_state(self):
+        credentials = self.make_credentials()
+
+        # No token acquired yet.
+        assert not credentials.token
+        assert not credentials.valid
+        # Expiration hasn't been set yet.
+        assert not credentials.expiry
+        assert not credentials.expired
+        # No quota project ID set.
+        assert not credentials.quota_project_id
+
+    def test_with_quota_project(self):
+        credentials = self.make_credentials()
+
+        assert not credentials.quota_project_id
+
+        quota_project_creds = credentials.with_quota_project("project-foo")
+
+        assert quota_project_creds.quota_project_id == "project-foo"
+
+    @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
+    def test_refresh(self, unused_utcnow):
+        response = SUCCESS_RESPONSE.copy()
+        # Test custom expiration to confirm expiry is set correctly.
+        response["expires_in"] = 2800
+        expected_expiry = datetime.datetime.min + datetime.timedelta(
+            seconds=response["expires_in"]
+        )
+        headers = {"Content-Type": "application/x-www-form-urlencoded"}
+        request_data = {
+            "grant_type": GRANT_TYPE,
+            "subject_token": "ACCESS_TOKEN_1",
+            "subject_token_type": SUBJECT_TOKEN_TYPE,
+            "requested_token_type": REQUESTED_TOKEN_TYPE,
+            "options": urllib.parse.quote(json.dumps(CREDENTIAL_ACCESS_BOUNDARY_JSON)),
+        }
+        request = self.make_mock_request(status=http_client.OK, data=response)
+        source_credentials = SourceCredentials()
+        credentials = self.make_credentials(source_credentials=source_credentials)
+
+        # Spy on calls to source credentials refresh to confirm the expected request
+        # instance is used.
+        with mock.patch.object(
+            source_credentials, "refresh", wraps=source_credentials.refresh
+        ) as wrapped_souce_cred_refresh:
+            credentials.refresh(request)
+
+            self.assert_request_kwargs(request.call_args[1], headers, request_data)
+            assert credentials.valid
+            assert credentials.expiry == expected_expiry
+            assert not credentials.expired
+            assert credentials.token == response["access_token"]
+            # Confirm source credentials called with the same request instance.
+            wrapped_souce_cred_refresh.assert_called_with(request)
+
+    @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
+    def test_refresh_without_response_expires_in(self, unused_utcnow):
+        response = SUCCESS_RESPONSE.copy()
+        # Simulate the response is missing the expires_in field.
+        # The downscoped token expiration should match the source credentials
+        # expiration.
+        del response["expires_in"]
+        expected_expires_in = 1800
+        # Simulate the source credentials generates a token with 1800 second
+        # expiration time. The generated downscoped token should have the same
+        # expiration time.
+        source_credentials = SourceCredentials(expires_in=expected_expires_in)
+        expected_expiry = datetime.datetime.min + datetime.timedelta(
+            seconds=expected_expires_in
+        )
+        headers = {"Content-Type": "application/x-www-form-urlencoded"}
+        request_data = {
+            "grant_type": GRANT_TYPE,
+            "subject_token": "ACCESS_TOKEN_1",
+            "subject_token_type": SUBJECT_TOKEN_TYPE,
+            "requested_token_type": REQUESTED_TOKEN_TYPE,
+            "options": urllib.parse.quote(json.dumps(CREDENTIAL_ACCESS_BOUNDARY_JSON)),
+        }
+        request = self.make_mock_request(status=http_client.OK, data=response)
+        credentials = self.make_credentials(source_credentials=source_credentials)
+
+        # Spy on calls to source credentials refresh to confirm the expected request
+        # instance is used.
+        with mock.patch.object(
+            source_credentials, "refresh", wraps=source_credentials.refresh
+        ) as wrapped_souce_cred_refresh:
+            credentials.refresh(request)
+
+            self.assert_request_kwargs(request.call_args[1], headers, request_data)
+            assert credentials.valid
+            assert credentials.expiry == expected_expiry
+            assert not credentials.expired
+            assert credentials.token == response["access_token"]
+            # Confirm source credentials called with the same request instance.
+            wrapped_souce_cred_refresh.assert_called_with(request)
+
+    def test_refresh_token_exchange_error(self):
+        request = self.make_mock_request(
+            status=http_client.BAD_REQUEST, data=ERROR_RESPONSE
+        )
+        credentials = self.make_credentials()
+
+        with pytest.raises(exceptions.OAuthError) as excinfo:
+            credentials.refresh(request)
+
+        assert excinfo.match(
+            r"Error code invalid_grant: Subject token is invalid. - https://tools.ietf.org/html/rfc6749"
+        )
+        assert not credentials.expired
+        assert credentials.token is None
+
+    def test_refresh_source_credentials_refresh_error(self):
+        # Initialize downscoped credentials with source credentials that raise
+        # an error on refresh.
+        credentials = self.make_credentials(
+            source_credentials=SourceCredentials(raise_error=True)
+        )
+
+        with pytest.raises(exceptions.RefreshError) as excinfo:
+            credentials.refresh(mock.sentinel.request)
+
+        assert excinfo.match(r"Failed to refresh access token in source credentials.")
+        assert not credentials.expired
+        assert credentials.token is None
+
+    def test_apply_without_quota_project_id(self):
+        headers = {}
+        request = self.make_mock_request(status=http_client.OK, data=SUCCESS_RESPONSE)
+        credentials = self.make_credentials()
+
+        credentials.refresh(request)
+        credentials.apply(headers)
+
+        assert headers == {
+            "authorization": "Bearer {}".format(SUCCESS_RESPONSE["access_token"])
+        }
+
+    def test_apply_with_quota_project_id(self):
+        headers = {"other": "header-value"}
+        request = self.make_mock_request(status=http_client.OK, data=SUCCESS_RESPONSE)
+        credentials = self.make_credentials(quota_project_id=QUOTA_PROJECT_ID)
+
+        credentials.refresh(request)
+        credentials.apply(headers)
+
+        assert headers == {
+            "other": "header-value",
+            "authorization": "Bearer {}".format(SUCCESS_RESPONSE["access_token"]),
+            "x-goog-user-project": QUOTA_PROJECT_ID,
+        }
+
+    def test_before_request(self):
+        headers = {"other": "header-value"}
+        request = self.make_mock_request(status=http_client.OK, data=SUCCESS_RESPONSE)
+        credentials = self.make_credentials()
+
+        # First call should call refresh, setting the token.
+        credentials.before_request(request, "POST", "https://example.com/api", headers)
+
+        assert headers == {
+            "other": "header-value",
+            "authorization": "Bearer {}".format(SUCCESS_RESPONSE["access_token"]),
+        }
+
+        # Second call shouldn't call refresh (request should be untouched).
+        credentials.before_request(
+            mock.sentinel.request, "POST", "https://example.com/api", headers
+        )
+
+        assert headers == {
+            "other": "header-value",
+            "authorization": "Bearer {}".format(SUCCESS_RESPONSE["access_token"]),
+        }
+
+    @mock.patch("google.auth._helpers.utcnow")
+    def test_before_request_expired(self, utcnow):
+        headers = {}
+        request = self.make_mock_request(status=http_client.OK, data=SUCCESS_RESPONSE)
+        credentials = self.make_credentials()
+        credentials.token = "token"
+        utcnow.return_value = datetime.datetime.min
+        # Set the expiration to one second more than now plus the clock skew
+        # accommodation. These credentials should be valid.
+        credentials.expiry = (
+            datetime.datetime.min
+            + _helpers.REFRESH_THRESHOLD
+            + datetime.timedelta(seconds=1)
+        )
+
+        assert credentials.valid
+        assert not credentials.expired
+
+        credentials.before_request(request, "POST", "https://example.com/api", headers)
+
+        # Cached token should be used.
+        assert headers == {"authorization": "Bearer token"}
+
+        # Next call should simulate 1 second passed.
+        utcnow.return_value = datetime.datetime.min + datetime.timedelta(seconds=1)
+
+        assert not credentials.valid
+        assert credentials.expired
+
+        credentials.before_request(request, "POST", "https://example.com/api", headers)
+
+        # New token should be retrieved.
+        assert headers == {
+            "authorization": "Bearer {}".format(SUCCESS_RESPONSE["access_token"])
+        }
diff --git a/tests/test_external_account.py b/tests/test_external_account.py
new file mode 100644
index 0000000..3c34f99
--- /dev/null
+++ b/tests/test_external_account.py
@@ -0,0 +1,1624 @@
+# 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.
+
+import datetime
+import json
+
+import mock
+import pytest
+from six.moves import http_client
+from six.moves import urllib
+
+from google.auth import _helpers
+from google.auth import exceptions
+from google.auth import external_account
+from google.auth import transport
+
+
+CLIENT_ID = "username"
+CLIENT_SECRET = "password"
+# Base64 encoding of "username:password"
+BASIC_AUTH_ENCODING = "dXNlcm5hbWU6cGFzc3dvcmQ="
+SERVICE_ACCOUNT_EMAIL = "[email protected]"
+# List of valid workforce pool audiences.
+TEST_USER_AUDIENCES = [
+    "//iam.googleapis.com/locations/global/workforcePools/pool-id/providers/provider-id",
+    "//iam.googleapis.com/locations/eu/workforcePools/pool-id/providers/provider-id",
+    "//iam.googleapis.com/locations/eu/workforcePools/workloadIdentityPools/providers/provider-id",
+]
+# Workload identity pool audiences or invalid workforce pool audiences.
+TEST_NON_USER_AUDIENCES = [
+    # Legacy K8s audience format.
+    "identitynamespace:1f12345:my_provider",
+    (
+        "//iam.googleapis.com/projects/123456/locations/"
+        "global/workloadIdentityPools/pool-id/providers/"
+        "provider-id"
+    ),
+    (
+        "//iam.googleapis.com/projects/123456/locations/"
+        "eu/workloadIdentityPools/pool-id/providers/"
+        "provider-id"
+    ),
+    # Pool ID with workforcePools string.
+    (
+        "//iam.googleapis.com/projects/123456/locations/"
+        "global/workloadIdentityPools/workforcePools/providers/"
+        "provider-id"
+    ),
+    # Unrealistic / incorrect workforce pool audiences.
+    "//iamgoogleapis.com/locations/eu/workforcePools/pool-id/providers/provider-id",
+    "//iam.googleapiscom/locations/eu/workforcePools/pool-id/providers/provider-id",
+    "//iam.googleapis.com/locations/workforcePools/pool-id/providers/provider-id",
+    "//iam.googleapis.com/locations/eu/workforcePool/pool-id/providers/provider-id",
+    "//iam.googleapis.com/locations//workforcePool/pool-id/providers/provider-id",
+]
+
+
+class CredentialsImpl(external_account.Credentials):
+    def __init__(
+        self,
+        audience,
+        subject_token_type,
+        token_url,
+        credential_source,
+        service_account_impersonation_url=None,
+        client_id=None,
+        client_secret=None,
+        quota_project_id=None,
+        scopes=None,
+        default_scopes=None,
+        workforce_pool_user_project=None,
+    ):
+        super(CredentialsImpl, self).__init__(
+            audience=audience,
+            subject_token_type=subject_token_type,
+            token_url=token_url,
+            credential_source=credential_source,
+            service_account_impersonation_url=service_account_impersonation_url,
+            client_id=client_id,
+            client_secret=client_secret,
+            quota_project_id=quota_project_id,
+            scopes=scopes,
+            default_scopes=default_scopes,
+            workforce_pool_user_project=workforce_pool_user_project,
+        )
+        self._counter = 0
+
+    def retrieve_subject_token(self, request):
+        counter = self._counter
+        self._counter += 1
+        return "subject_token_{}".format(counter)
+
+
+class TestCredentials(object):
+    TOKEN_URL = "https://sts.googleapis.com/v1/token"
+    PROJECT_NUMBER = "123456"
+    POOL_ID = "POOL_ID"
+    PROVIDER_ID = "PROVIDER_ID"
+    AUDIENCE = (
+        "//iam.googleapis.com/projects/{}"
+        "/locations/global/workloadIdentityPools/{}"
+        "/providers/{}"
+    ).format(PROJECT_NUMBER, POOL_ID, PROVIDER_ID)
+    WORKFORCE_AUDIENCE = (
+        "//iam.googleapis.com/locations/global/workforcePools/{}/providers/{}"
+    ).format(POOL_ID, PROVIDER_ID)
+    WORKFORCE_POOL_USER_PROJECT = "WORKFORCE_POOL_USER_PROJECT_NUMBER"
+    SUBJECT_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:jwt"
+    WORKFORCE_SUBJECT_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:id_token"
+    CREDENTIAL_SOURCE = {"file": "/var/run/secrets/goog.id/token"}
+    SUCCESS_RESPONSE = {
+        "access_token": "ACCESS_TOKEN",
+        "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
+        "token_type": "Bearer",
+        "expires_in": 3600,
+        "scope": "scope1 scope2",
+    }
+    ERROR_RESPONSE = {
+        "error": "invalid_request",
+        "error_description": "Invalid subject token",
+        "error_uri": "https://tools.ietf.org/html/rfc6749",
+    }
+    QUOTA_PROJECT_ID = "QUOTA_PROJECT_ID"
+    SERVICE_ACCOUNT_IMPERSONATION_URL = (
+        "https://us-east1-iamcredentials.googleapis.com/v1/projects/-"
+        + "/serviceAccounts/{}:generateAccessToken".format(SERVICE_ACCOUNT_EMAIL)
+    )
+    SCOPES = ["scope1", "scope2"]
+    IMPERSONATION_ERROR_RESPONSE = {
+        "error": {
+            "code": 400,
+            "message": "Request contains an invalid argument",
+            "status": "INVALID_ARGUMENT",
+        }
+    }
+    PROJECT_ID = "my-proj-id"
+    CLOUD_RESOURCE_MANAGER_URL = (
+        "https://cloudresourcemanager.googleapis.com/v1/projects/"
+    )
+    CLOUD_RESOURCE_MANAGER_SUCCESS_RESPONSE = {
+        "projectNumber": PROJECT_NUMBER,
+        "projectId": PROJECT_ID,
+        "lifecycleState": "ACTIVE",
+        "name": "project-name",
+        "createTime": "2018-11-06T04:42:54.109Z",
+        "parent": {"type": "folder", "id": "12345678901"},
+    }
+
+    @classmethod
+    def make_credentials(
+        cls,
+        client_id=None,
+        client_secret=None,
+        quota_project_id=None,
+        scopes=None,
+        default_scopes=None,
+        service_account_impersonation_url=None,
+    ):
+        return CredentialsImpl(
+            audience=cls.AUDIENCE,
+            subject_token_type=cls.SUBJECT_TOKEN_TYPE,
+            token_url=cls.TOKEN_URL,
+            service_account_impersonation_url=service_account_impersonation_url,
+            credential_source=cls.CREDENTIAL_SOURCE,
+            client_id=client_id,
+            client_secret=client_secret,
+            quota_project_id=quota_project_id,
+            scopes=scopes,
+            default_scopes=default_scopes,
+        )
+
+    @classmethod
+    def make_workforce_pool_credentials(
+        cls,
+        client_id=None,
+        client_secret=None,
+        quota_project_id=None,
+        scopes=None,
+        default_scopes=None,
+        service_account_impersonation_url=None,
+        workforce_pool_user_project=None,
+    ):
+        return CredentialsImpl(
+            audience=cls.WORKFORCE_AUDIENCE,
+            subject_token_type=cls.WORKFORCE_SUBJECT_TOKEN_TYPE,
+            token_url=cls.TOKEN_URL,
+            service_account_impersonation_url=service_account_impersonation_url,
+            credential_source=cls.CREDENTIAL_SOURCE,
+            client_id=client_id,
+            client_secret=client_secret,
+            quota_project_id=quota_project_id,
+            scopes=scopes,
+            default_scopes=default_scopes,
+            workforce_pool_user_project=workforce_pool_user_project,
+        )
+
+    @classmethod
+    def make_mock_request(
+        cls,
+        status=http_client.OK,
+        data=None,
+        impersonation_status=None,
+        impersonation_data=None,
+        cloud_resource_manager_status=None,
+        cloud_resource_manager_data=None,
+    ):
+        # STS token exchange request.
+        token_response = mock.create_autospec(transport.Response, instance=True)
+        token_response.status = status
+        token_response.data = json.dumps(data).encode("utf-8")
+        responses = [token_response]
+
+        # If service account impersonation is requested, mock the expected response.
+        if impersonation_status:
+            impersonation_response = mock.create_autospec(
+                transport.Response, instance=True
+            )
+            impersonation_response.status = impersonation_status
+            impersonation_response.data = json.dumps(impersonation_data).encode("utf-8")
+            responses.append(impersonation_response)
+
+        # If cloud resource manager is requested, mock the expected response.
+        if cloud_resource_manager_status:
+            cloud_resource_manager_response = mock.create_autospec(
+                transport.Response, instance=True
+            )
+            cloud_resource_manager_response.status = cloud_resource_manager_status
+            cloud_resource_manager_response.data = json.dumps(
+                cloud_resource_manager_data
+            ).encode("utf-8")
+            responses.append(cloud_resource_manager_response)
+
+        request = mock.create_autospec(transport.Request)
+        request.side_effect = responses
+
+        return request
+
+    @classmethod
+    def assert_token_request_kwargs(cls, request_kwargs, headers, request_data):
+        assert request_kwargs["url"] == cls.TOKEN_URL
+        assert request_kwargs["method"] == "POST"
+        assert request_kwargs["headers"] == headers
+        assert request_kwargs["body"] is not None
+        body_tuples = urllib.parse.parse_qsl(request_kwargs["body"])
+        for (k, v) in body_tuples:
+            assert v.decode("utf-8") == request_data[k.decode("utf-8")]
+        assert len(body_tuples) == len(request_data.keys())
+
+    @classmethod
+    def assert_impersonation_request_kwargs(cls, request_kwargs, headers, request_data):
+        assert request_kwargs["url"] == cls.SERVICE_ACCOUNT_IMPERSONATION_URL
+        assert request_kwargs["method"] == "POST"
+        assert request_kwargs["headers"] == headers
+        assert request_kwargs["body"] is not None
+        body_json = json.loads(request_kwargs["body"].decode("utf-8"))
+        assert body_json == request_data
+
+    @classmethod
+    def assert_resource_manager_request_kwargs(
+        cls, request_kwargs, project_number, headers
+    ):
+        assert request_kwargs["url"] == cls.CLOUD_RESOURCE_MANAGER_URL + project_number
+        assert request_kwargs["method"] == "GET"
+        assert request_kwargs["headers"] == headers
+        assert "body" not in request_kwargs
+
+    def test_default_state(self):
+        credentials = self.make_credentials()
+
+        # Not token acquired yet
+        assert not credentials.token
+        assert not credentials.valid
+        # Expiration hasn't been set yet
+        assert not credentials.expiry
+        assert not credentials.expired
+        # Scopes are required
+        assert not credentials.scopes
+        assert credentials.requires_scopes
+        assert not credentials.quota_project_id
+
+    def test_nonworkforce_with_workforce_pool_user_project(self):
+        with pytest.raises(ValueError) as excinfo:
+            CredentialsImpl(
+                audience=self.AUDIENCE,
+                subject_token_type=self.SUBJECT_TOKEN_TYPE,
+                token_url=self.TOKEN_URL,
+                credential_source=self.CREDENTIAL_SOURCE,
+                workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT,
+            )
+
+        assert excinfo.match(
+            "workforce_pool_user_project should not be set for non-workforce "
+            "pool credentials"
+        )
+
+    def test_with_scopes(self):
+        credentials = self.make_credentials()
+
+        assert not credentials.scopes
+        assert credentials.requires_scopes
+
+        scoped_credentials = credentials.with_scopes(["email"])
+
+        assert scoped_credentials.has_scopes(["email"])
+        assert not scoped_credentials.requires_scopes
+
+    def test_with_scopes_workforce_pool(self):
+        credentials = self.make_workforce_pool_credentials(
+            workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT
+        )
+
+        assert not credentials.scopes
+        assert credentials.requires_scopes
+
+        scoped_credentials = credentials.with_scopes(["email"])
+
+        assert scoped_credentials.has_scopes(["email"])
+        assert not scoped_credentials.requires_scopes
+        assert (
+            scoped_credentials.info.get("workforce_pool_user_project")
+            == self.WORKFORCE_POOL_USER_PROJECT
+        )
+
+    def test_with_scopes_using_user_and_default_scopes(self):
+        credentials = self.make_credentials()
+
+        assert not credentials.scopes
+        assert credentials.requires_scopes
+
+        scoped_credentials = credentials.with_scopes(
+            ["email"], default_scopes=["profile"]
+        )
+
+        assert scoped_credentials.has_scopes(["email"])
+        assert not scoped_credentials.has_scopes(["profile"])
+        assert not scoped_credentials.requires_scopes
+        assert scoped_credentials.scopes == ["email"]
+        assert scoped_credentials.default_scopes == ["profile"]
+
+    def test_with_scopes_using_default_scopes_only(self):
+        credentials = self.make_credentials()
+
+        assert not credentials.scopes
+        assert credentials.requires_scopes
+
+        scoped_credentials = credentials.with_scopes(None, default_scopes=["profile"])
+
+        assert scoped_credentials.has_scopes(["profile"])
+        assert not scoped_credentials.requires_scopes
+
+    def test_with_scopes_full_options_propagated(self):
+        credentials = self.make_credentials(
+            client_id=CLIENT_ID,
+            client_secret=CLIENT_SECRET,
+            quota_project_id=self.QUOTA_PROJECT_ID,
+            scopes=self.SCOPES,
+            default_scopes=["default1"],
+            service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL,
+        )
+
+        with mock.patch.object(
+            external_account.Credentials, "__init__", return_value=None
+        ) as mock_init:
+            credentials.with_scopes(["email"], ["default2"])
+
+        # Confirm with_scopes initialized the credential with the expected
+        # parameters and scopes.
+        mock_init.assert_called_once_with(
+            audience=self.AUDIENCE,
+            subject_token_type=self.SUBJECT_TOKEN_TYPE,
+            token_url=self.TOKEN_URL,
+            credential_source=self.CREDENTIAL_SOURCE,
+            service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL,
+            client_id=CLIENT_ID,
+            client_secret=CLIENT_SECRET,
+            quota_project_id=self.QUOTA_PROJECT_ID,
+            scopes=["email"],
+            default_scopes=["default2"],
+            workforce_pool_user_project=None,
+        )
+
+    def test_with_quota_project(self):
+        credentials = self.make_credentials()
+
+        assert not credentials.scopes
+        assert not credentials.quota_project_id
+
+        quota_project_creds = credentials.with_quota_project("project-foo")
+
+        assert quota_project_creds.quota_project_id == "project-foo"
+
+    def test_with_quota_project_workforce_pool(self):
+        credentials = self.make_workforce_pool_credentials(
+            workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT
+        )
+
+        assert not credentials.scopes
+        assert not credentials.quota_project_id
+
+        quota_project_creds = credentials.with_quota_project("project-foo")
+
+        assert quota_project_creds.quota_project_id == "project-foo"
+        assert (
+            quota_project_creds.info.get("workforce_pool_user_project")
+            == self.WORKFORCE_POOL_USER_PROJECT
+        )
+
+    def test_with_quota_project_full_options_propagated(self):
+        credentials = self.make_credentials(
+            client_id=CLIENT_ID,
+            client_secret=CLIENT_SECRET,
+            quota_project_id=self.QUOTA_PROJECT_ID,
+            scopes=self.SCOPES,
+            default_scopes=["default1"],
+            service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL,
+        )
+
+        with mock.patch.object(
+            external_account.Credentials, "__init__", return_value=None
+        ) as mock_init:
+            credentials.with_quota_project("project-foo")
+
+        # Confirm with_quota_project initialized the credential with the
+        # expected parameters and quota project ID.
+        mock_init.assert_called_once_with(
+            audience=self.AUDIENCE,
+            subject_token_type=self.SUBJECT_TOKEN_TYPE,
+            token_url=self.TOKEN_URL,
+            credential_source=self.CREDENTIAL_SOURCE,
+            service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL,
+            client_id=CLIENT_ID,
+            client_secret=CLIENT_SECRET,
+            quota_project_id="project-foo",
+            scopes=self.SCOPES,
+            default_scopes=["default1"],
+            workforce_pool_user_project=None,
+        )
+
+    def test_with_invalid_impersonation_target_principal(self):
+        invalid_url = "https://iamcredentials.googleapis.com/v1/invalid"
+
+        with pytest.raises(exceptions.RefreshError) as excinfo:
+            self.make_credentials(service_account_impersonation_url=invalid_url)
+
+        assert excinfo.match(
+            r"Unable to determine target principal from service account impersonation URL."
+        )
+
+    def test_info(self):
+        credentials = self.make_credentials()
+
+        assert credentials.info == {
+            "type": "external_account",
+            "audience": self.AUDIENCE,
+            "subject_token_type": self.SUBJECT_TOKEN_TYPE,
+            "token_url": self.TOKEN_URL,
+            "credential_source": self.CREDENTIAL_SOURCE.copy(),
+        }
+
+    def test_info_workforce_pool(self):
+        credentials = self.make_workforce_pool_credentials(
+            workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT
+        )
+
+        assert credentials.info == {
+            "type": "external_account",
+            "audience": self.WORKFORCE_AUDIENCE,
+            "subject_token_type": self.WORKFORCE_SUBJECT_TOKEN_TYPE,
+            "token_url": self.TOKEN_URL,
+            "credential_source": self.CREDENTIAL_SOURCE.copy(),
+            "workforce_pool_user_project": self.WORKFORCE_POOL_USER_PROJECT,
+        }
+
+    def test_info_with_full_options(self):
+        credentials = self.make_credentials(
+            client_id=CLIENT_ID,
+            client_secret=CLIENT_SECRET,
+            quota_project_id=self.QUOTA_PROJECT_ID,
+            service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL,
+        )
+
+        assert credentials.info == {
+            "type": "external_account",
+            "audience": self.AUDIENCE,
+            "subject_token_type": self.SUBJECT_TOKEN_TYPE,
+            "token_url": self.TOKEN_URL,
+            "service_account_impersonation_url": self.SERVICE_ACCOUNT_IMPERSONATION_URL,
+            "credential_source": self.CREDENTIAL_SOURCE.copy(),
+            "quota_project_id": self.QUOTA_PROJECT_ID,
+            "client_id": CLIENT_ID,
+            "client_secret": CLIENT_SECRET,
+        }
+
+    def test_service_account_email_without_impersonation(self):
+        credentials = self.make_credentials()
+
+        assert credentials.service_account_email is None
+
+    def test_service_account_email_with_impersonation(self):
+        credentials = self.make_credentials(
+            service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL
+        )
+
+        assert credentials.service_account_email == SERVICE_ACCOUNT_EMAIL
+
+    @pytest.mark.parametrize("audience", TEST_NON_USER_AUDIENCES)
+    def test_is_user_with_non_users(self, audience):
+        credentials = CredentialsImpl(
+            audience=audience,
+            subject_token_type=self.SUBJECT_TOKEN_TYPE,
+            token_url=self.TOKEN_URL,
+            credential_source=self.CREDENTIAL_SOURCE,
+        )
+
+        assert credentials.is_user is False
+
+    @pytest.mark.parametrize("audience", TEST_USER_AUDIENCES)
+    def test_is_user_with_users(self, audience):
+        credentials = CredentialsImpl(
+            audience=audience,
+            subject_token_type=self.SUBJECT_TOKEN_TYPE,
+            token_url=self.TOKEN_URL,
+            credential_source=self.CREDENTIAL_SOURCE,
+        )
+
+        assert credentials.is_user is True
+
+    @pytest.mark.parametrize("audience", TEST_USER_AUDIENCES)
+    def test_is_user_with_users_and_impersonation(self, audience):
+        # Initialize the credentials with service account impersonation.
+        credentials = CredentialsImpl(
+            audience=audience,
+            subject_token_type=self.SUBJECT_TOKEN_TYPE,
+            token_url=self.TOKEN_URL,
+            credential_source=self.CREDENTIAL_SOURCE,
+            service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL,
+        )
+
+        # Even though the audience is for a workforce pool, since service account
+        # impersonation is used, the credentials will represent a service account and
+        # not a user.
+        assert credentials.is_user is False
+
+    @pytest.mark.parametrize("audience", TEST_NON_USER_AUDIENCES)
+    def test_is_workforce_pool_with_non_users(self, audience):
+        credentials = CredentialsImpl(
+            audience=audience,
+            subject_token_type=self.SUBJECT_TOKEN_TYPE,
+            token_url=self.TOKEN_URL,
+            credential_source=self.CREDENTIAL_SOURCE,
+        )
+
+        assert credentials.is_workforce_pool is False
+
+    @pytest.mark.parametrize("audience", TEST_USER_AUDIENCES)
+    def test_is_workforce_pool_with_users(self, audience):
+        credentials = CredentialsImpl(
+            audience=audience,
+            subject_token_type=self.SUBJECT_TOKEN_TYPE,
+            token_url=self.TOKEN_URL,
+            credential_source=self.CREDENTIAL_SOURCE,
+        )
+
+        assert credentials.is_workforce_pool is True
+
+    @pytest.mark.parametrize("audience", TEST_USER_AUDIENCES)
+    def test_is_workforce_pool_with_users_and_impersonation(self, audience):
+        # Initialize the credentials with workforce audience and service account
+        # impersonation.
+        credentials = CredentialsImpl(
+            audience=audience,
+            subject_token_type=self.SUBJECT_TOKEN_TYPE,
+            token_url=self.TOKEN_URL,
+            credential_source=self.CREDENTIAL_SOURCE,
+            service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL,
+        )
+
+        # Even though impersonation is used, is_workforce_pool should still return True.
+        assert credentials.is_workforce_pool is True
+
+    @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
+    def test_refresh_without_client_auth_success(self, unused_utcnow):
+        response = self.SUCCESS_RESPONSE.copy()
+        # Test custom expiration to confirm expiry is set correctly.
+        response["expires_in"] = 2800
+        expected_expiry = datetime.datetime.min + datetime.timedelta(
+            seconds=response["expires_in"]
+        )
+        headers = {"Content-Type": "application/x-www-form-urlencoded"}
+        request_data = {
+            "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
+            "audience": self.AUDIENCE,
+            "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
+            "subject_token": "subject_token_0",
+            "subject_token_type": self.SUBJECT_TOKEN_TYPE,
+        }
+        request = self.make_mock_request(status=http_client.OK, data=response)
+        credentials = self.make_credentials()
+
+        credentials.refresh(request)
+
+        self.assert_token_request_kwargs(request.call_args[1], headers, request_data)
+        assert credentials.valid
+        assert credentials.expiry == expected_expiry
+        assert not credentials.expired
+        assert credentials.token == response["access_token"]
+
+    @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
+    def test_refresh_workforce_without_client_auth_success(self, unused_utcnow):
+        response = self.SUCCESS_RESPONSE.copy()
+        # Test custom expiration to confirm expiry is set correctly.
+        response["expires_in"] = 2800
+        expected_expiry = datetime.datetime.min + datetime.timedelta(
+            seconds=response["expires_in"]
+        )
+        headers = {"Content-Type": "application/x-www-form-urlencoded"}
+        request_data = {
+            "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
+            "audience": self.WORKFORCE_AUDIENCE,
+            "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
+            "subject_token": "subject_token_0",
+            "subject_token_type": self.WORKFORCE_SUBJECT_TOKEN_TYPE,
+            "options": urllib.parse.quote(
+                json.dumps({"userProject": self.WORKFORCE_POOL_USER_PROJECT})
+            ),
+        }
+        request = self.make_mock_request(status=http_client.OK, data=response)
+        credentials = self.make_workforce_pool_credentials(
+            workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT
+        )
+
+        credentials.refresh(request)
+
+        self.assert_token_request_kwargs(request.call_args[1], headers, request_data)
+        assert credentials.valid
+        assert credentials.expiry == expected_expiry
+        assert not credentials.expired
+        assert credentials.token == response["access_token"]
+
+    @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
+    def test_refresh_workforce_with_client_auth_success(self, unused_utcnow):
+        response = self.SUCCESS_RESPONSE.copy()
+        # Test custom expiration to confirm expiry is set correctly.
+        response["expires_in"] = 2800
+        expected_expiry = datetime.datetime.min + datetime.timedelta(
+            seconds=response["expires_in"]
+        )
+        headers = {
+            "Content-Type": "application/x-www-form-urlencoded",
+            "Authorization": "Basic {}".format(BASIC_AUTH_ENCODING),
+        }
+        request_data = {
+            "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
+            "audience": self.WORKFORCE_AUDIENCE,
+            "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
+            "subject_token": "subject_token_0",
+            "subject_token_type": self.WORKFORCE_SUBJECT_TOKEN_TYPE,
+        }
+        request = self.make_mock_request(status=http_client.OK, data=response)
+        # Client Auth will have higher priority over workforce_pool_user_project.
+        credentials = self.make_workforce_pool_credentials(
+            client_id=CLIENT_ID,
+            client_secret=CLIENT_SECRET,
+            workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT,
+        )
+
+        credentials.refresh(request)
+
+        self.assert_token_request_kwargs(request.call_args[1], headers, request_data)
+        assert credentials.valid
+        assert credentials.expiry == expected_expiry
+        assert not credentials.expired
+        assert credentials.token == response["access_token"]
+
+    @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
+    def test_refresh_workforce_with_client_auth_and_no_workforce_project_success(
+        self, unused_utcnow
+    ):
+        response = self.SUCCESS_RESPONSE.copy()
+        # Test custom expiration to confirm expiry is set correctly.
+        response["expires_in"] = 2800
+        expected_expiry = datetime.datetime.min + datetime.timedelta(
+            seconds=response["expires_in"]
+        )
+        headers = {
+            "Content-Type": "application/x-www-form-urlencoded",
+            "Authorization": "Basic {}".format(BASIC_AUTH_ENCODING),
+        }
+        request_data = {
+            "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
+            "audience": self.WORKFORCE_AUDIENCE,
+            "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
+            "subject_token": "subject_token_0",
+            "subject_token_type": self.WORKFORCE_SUBJECT_TOKEN_TYPE,
+        }
+        request = self.make_mock_request(status=http_client.OK, data=response)
+        # Client Auth will be sufficient for user project determination.
+        credentials = self.make_workforce_pool_credentials(
+            client_id=CLIENT_ID,
+            client_secret=CLIENT_SECRET,
+            workforce_pool_user_project=None,
+        )
+
+        credentials.refresh(request)
+
+        self.assert_token_request_kwargs(request.call_args[1], headers, request_data)
+        assert credentials.valid
+        assert credentials.expiry == expected_expiry
+        assert not credentials.expired
+        assert credentials.token == response["access_token"]
+
+    def test_refresh_impersonation_without_client_auth_success(self):
+        # Simulate service account access token expires in 2800 seconds.
+        expire_time = (
+            _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=2800)
+        ).isoformat("T") + "Z"
+        expected_expiry = datetime.datetime.strptime(expire_time, "%Y-%m-%dT%H:%M:%SZ")
+        # STS token exchange request/response.
+        token_response = self.SUCCESS_RESPONSE.copy()
+        token_headers = {"Content-Type": "application/x-www-form-urlencoded"}
+        token_request_data = {
+            "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
+            "audience": self.AUDIENCE,
+            "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
+            "subject_token": "subject_token_0",
+            "subject_token_type": self.SUBJECT_TOKEN_TYPE,
+            "scope": "https://www.googleapis.com/auth/iam",
+        }
+        # Service account impersonation request/response.
+        impersonation_response = {
+            "accessToken": "SA_ACCESS_TOKEN",
+            "expireTime": expire_time,
+        }
+        impersonation_headers = {
+            "Content-Type": "application/json",
+            "authorization": "Bearer {}".format(token_response["access_token"]),
+        }
+        impersonation_request_data = {
+            "delegates": None,
+            "scope": self.SCOPES,
+            "lifetime": "3600s",
+        }
+        # Initialize mock request to handle token exchange and service account
+        # impersonation request.
+        request = self.make_mock_request(
+            status=http_client.OK,
+            data=token_response,
+            impersonation_status=http_client.OK,
+            impersonation_data=impersonation_response,
+        )
+        # Initialize credentials with service account impersonation.
+        credentials = self.make_credentials(
+            service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL,
+            scopes=self.SCOPES,
+        )
+
+        credentials.refresh(request)
+
+        # Only 2 requests should be processed.
+        assert len(request.call_args_list) == 2
+        # Verify token exchange request parameters.
+        self.assert_token_request_kwargs(
+            request.call_args_list[0][1], token_headers, token_request_data
+        )
+        # Verify service account impersonation request parameters.
+        self.assert_impersonation_request_kwargs(
+            request.call_args_list[1][1],
+            impersonation_headers,
+            impersonation_request_data,
+        )
+        assert credentials.valid
+        assert credentials.expiry == expected_expiry
+        assert not credentials.expired
+        assert credentials.token == impersonation_response["accessToken"]
+
+    def test_refresh_workforce_impersonation_without_client_auth_success(self):
+        # Simulate service account access token expires in 2800 seconds.
+        expire_time = (
+            _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=2800)
+        ).isoformat("T") + "Z"
+        expected_expiry = datetime.datetime.strptime(expire_time, "%Y-%m-%dT%H:%M:%SZ")
+        # STS token exchange request/response.
+        token_response = self.SUCCESS_RESPONSE.copy()
+        token_headers = {"Content-Type": "application/x-www-form-urlencoded"}
+        token_request_data = {
+            "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
+            "audience": self.WORKFORCE_AUDIENCE,
+            "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
+            "subject_token": "subject_token_0",
+            "subject_token_type": self.WORKFORCE_SUBJECT_TOKEN_TYPE,
+            "scope": "https://www.googleapis.com/auth/iam",
+            "options": urllib.parse.quote(
+                json.dumps({"userProject": self.WORKFORCE_POOL_USER_PROJECT})
+            ),
+        }
+        # Service account impersonation request/response.
+        impersonation_response = {
+            "accessToken": "SA_ACCESS_TOKEN",
+            "expireTime": expire_time,
+        }
+        impersonation_headers = {
+            "Content-Type": "application/json",
+            "authorization": "Bearer {}".format(token_response["access_token"]),
+        }
+        impersonation_request_data = {
+            "delegates": None,
+            "scope": self.SCOPES,
+            "lifetime": "3600s",
+        }
+        # Initialize mock request to handle token exchange and service account
+        # impersonation request.
+        request = self.make_mock_request(
+            status=http_client.OK,
+            data=token_response,
+            impersonation_status=http_client.OK,
+            impersonation_data=impersonation_response,
+        )
+        # Initialize credentials with service account impersonation.
+        credentials = self.make_workforce_pool_credentials(
+            service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL,
+            scopes=self.SCOPES,
+            workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT,
+        )
+
+        credentials.refresh(request)
+
+        # Only 2 requests should be processed.
+        assert len(request.call_args_list) == 2
+        # Verify token exchange request parameters.
+        self.assert_token_request_kwargs(
+            request.call_args_list[0][1], token_headers, token_request_data
+        )
+        # Verify service account impersonation request parameters.
+        self.assert_impersonation_request_kwargs(
+            request.call_args_list[1][1],
+            impersonation_headers,
+            impersonation_request_data,
+        )
+        assert credentials.valid
+        assert credentials.expiry == expected_expiry
+        assert not credentials.expired
+        assert credentials.token == impersonation_response["accessToken"]
+
+    def test_refresh_without_client_auth_success_explicit_user_scopes_ignore_default_scopes(
+        self,
+    ):
+        headers = {"Content-Type": "application/x-www-form-urlencoded"}
+        request_data = {
+            "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
+            "audience": self.AUDIENCE,
+            "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
+            "scope": "scope1 scope2",
+            "subject_token": "subject_token_0",
+            "subject_token_type": self.SUBJECT_TOKEN_TYPE,
+        }
+        request = self.make_mock_request(
+            status=http_client.OK, data=self.SUCCESS_RESPONSE
+        )
+        credentials = self.make_credentials(
+            scopes=["scope1", "scope2"],
+            # Default scopes will be ignored in favor of user scopes.
+            default_scopes=["ignored"],
+        )
+
+        credentials.refresh(request)
+
+        self.assert_token_request_kwargs(request.call_args[1], headers, request_data)
+        assert credentials.valid
+        assert not credentials.expired
+        assert credentials.token == self.SUCCESS_RESPONSE["access_token"]
+        assert credentials.has_scopes(["scope1", "scope2"])
+        assert not credentials.has_scopes(["ignored"])
+
+    def test_refresh_without_client_auth_success_explicit_default_scopes_only(self):
+        headers = {"Content-Type": "application/x-www-form-urlencoded"}
+        request_data = {
+            "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
+            "audience": self.AUDIENCE,
+            "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
+            "scope": "scope1 scope2",
+            "subject_token": "subject_token_0",
+            "subject_token_type": self.SUBJECT_TOKEN_TYPE,
+        }
+        request = self.make_mock_request(
+            status=http_client.OK, data=self.SUCCESS_RESPONSE
+        )
+        credentials = self.make_credentials(
+            scopes=None,
+            # Default scopes will be used since user scopes are none.
+            default_scopes=["scope1", "scope2"],
+        )
+
+        credentials.refresh(request)
+
+        self.assert_token_request_kwargs(request.call_args[1], headers, request_data)
+        assert credentials.valid
+        assert not credentials.expired
+        assert credentials.token == self.SUCCESS_RESPONSE["access_token"]
+        assert credentials.has_scopes(["scope1", "scope2"])
+
+    def test_refresh_without_client_auth_error(self):
+        request = self.make_mock_request(
+            status=http_client.BAD_REQUEST, data=self.ERROR_RESPONSE
+        )
+        credentials = self.make_credentials()
+
+        with pytest.raises(exceptions.OAuthError) as excinfo:
+            credentials.refresh(request)
+
+        assert excinfo.match(
+            r"Error code invalid_request: Invalid subject token - https://tools.ietf.org/html/rfc6749"
+        )
+        assert not credentials.expired
+        assert credentials.token is None
+
+    def test_refresh_impersonation_without_client_auth_error(self):
+        request = self.make_mock_request(
+            status=http_client.OK,
+            data=self.SUCCESS_RESPONSE,
+            impersonation_status=http_client.BAD_REQUEST,
+            impersonation_data=self.IMPERSONATION_ERROR_RESPONSE,
+        )
+        credentials = self.make_credentials(
+            service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL,
+            scopes=self.SCOPES,
+        )
+
+        with pytest.raises(exceptions.RefreshError) as excinfo:
+            credentials.refresh(request)
+
+        assert excinfo.match(r"Unable to acquire impersonated credentials")
+        assert not credentials.expired
+        assert credentials.token is None
+
+    def test_refresh_with_client_auth_success(self):
+        headers = {
+            "Content-Type": "application/x-www-form-urlencoded",
+            "Authorization": "Basic {}".format(BASIC_AUTH_ENCODING),
+        }
+        request_data = {
+            "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
+            "audience": self.AUDIENCE,
+            "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
+            "subject_token": "subject_token_0",
+            "subject_token_type": self.SUBJECT_TOKEN_TYPE,
+        }
+        request = self.make_mock_request(
+            status=http_client.OK, data=self.SUCCESS_RESPONSE
+        )
+        credentials = self.make_credentials(
+            client_id=CLIENT_ID, client_secret=CLIENT_SECRET
+        )
+
+        credentials.refresh(request)
+
+        self.assert_token_request_kwargs(request.call_args[1], headers, request_data)
+        assert credentials.valid
+        assert not credentials.expired
+        assert credentials.token == self.SUCCESS_RESPONSE["access_token"]
+
+    def test_refresh_impersonation_with_client_auth_success_ignore_default_scopes(self):
+        # Simulate service account access token expires in 2800 seconds.
+        expire_time = (
+            _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=2800)
+        ).isoformat("T") + "Z"
+        expected_expiry = datetime.datetime.strptime(expire_time, "%Y-%m-%dT%H:%M:%SZ")
+        # STS token exchange request/response.
+        token_response = self.SUCCESS_RESPONSE.copy()
+        token_headers = {
+            "Content-Type": "application/x-www-form-urlencoded",
+            "Authorization": "Basic {}".format(BASIC_AUTH_ENCODING),
+        }
+        token_request_data = {
+            "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
+            "audience": self.AUDIENCE,
+            "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
+            "subject_token": "subject_token_0",
+            "subject_token_type": self.SUBJECT_TOKEN_TYPE,
+            "scope": "https://www.googleapis.com/auth/iam",
+        }
+        # Service account impersonation request/response.
+        impersonation_response = {
+            "accessToken": "SA_ACCESS_TOKEN",
+            "expireTime": expire_time,
+        }
+        impersonation_headers = {
+            "Content-Type": "application/json",
+            "authorization": "Bearer {}".format(token_response["access_token"]),
+        }
+        impersonation_request_data = {
+            "delegates": None,
+            "scope": self.SCOPES,
+            "lifetime": "3600s",
+        }
+        # Initialize mock request to handle token exchange and service account
+        # impersonation request.
+        request = self.make_mock_request(
+            status=http_client.OK,
+            data=token_response,
+            impersonation_status=http_client.OK,
+            impersonation_data=impersonation_response,
+        )
+        # Initialize credentials with service account impersonation and basic auth.
+        credentials = self.make_credentials(
+            client_id=CLIENT_ID,
+            client_secret=CLIENT_SECRET,
+            service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL,
+            scopes=self.SCOPES,
+            # Default scopes will be ignored since user scopes are specified.
+            default_scopes=["ignored"],
+        )
+
+        credentials.refresh(request)
+
+        # Only 2 requests should be processed.
+        assert len(request.call_args_list) == 2
+        # Verify token exchange request parameters.
+        self.assert_token_request_kwargs(
+            request.call_args_list[0][1], token_headers, token_request_data
+        )
+        # Verify service account impersonation request parameters.
+        self.assert_impersonation_request_kwargs(
+            request.call_args_list[1][1],
+            impersonation_headers,
+            impersonation_request_data,
+        )
+        assert credentials.valid
+        assert credentials.expiry == expected_expiry
+        assert not credentials.expired
+        assert credentials.token == impersonation_response["accessToken"]
+
+    def test_refresh_impersonation_with_client_auth_success_use_default_scopes(self):
+        # Simulate service account access token expires in 2800 seconds.
+        expire_time = (
+            _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=2800)
+        ).isoformat("T") + "Z"
+        expected_expiry = datetime.datetime.strptime(expire_time, "%Y-%m-%dT%H:%M:%SZ")
+        # STS token exchange request/response.
+        token_response = self.SUCCESS_RESPONSE.copy()
+        token_headers = {
+            "Content-Type": "application/x-www-form-urlencoded",
+            "Authorization": "Basic {}".format(BASIC_AUTH_ENCODING),
+        }
+        token_request_data = {
+            "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
+            "audience": self.AUDIENCE,
+            "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
+            "subject_token": "subject_token_0",
+            "subject_token_type": self.SUBJECT_TOKEN_TYPE,
+            "scope": "https://www.googleapis.com/auth/iam",
+        }
+        # Service account impersonation request/response.
+        impersonation_response = {
+            "accessToken": "SA_ACCESS_TOKEN",
+            "expireTime": expire_time,
+        }
+        impersonation_headers = {
+            "Content-Type": "application/json",
+            "authorization": "Bearer {}".format(token_response["access_token"]),
+        }
+        impersonation_request_data = {
+            "delegates": None,
+            "scope": self.SCOPES,
+            "lifetime": "3600s",
+        }
+        # Initialize mock request to handle token exchange and service account
+        # impersonation request.
+        request = self.make_mock_request(
+            status=http_client.OK,
+            data=token_response,
+            impersonation_status=http_client.OK,
+            impersonation_data=impersonation_response,
+        )
+        # Initialize credentials with service account impersonation and basic auth.
+        credentials = self.make_credentials(
+            client_id=CLIENT_ID,
+            client_secret=CLIENT_SECRET,
+            service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL,
+            scopes=None,
+            # Default scopes will be used since user specified scopes are none.
+            default_scopes=self.SCOPES,
+        )
+
+        credentials.refresh(request)
+
+        # Only 2 requests should be processed.
+        assert len(request.call_args_list) == 2
+        # Verify token exchange request parameters.
+        self.assert_token_request_kwargs(
+            request.call_args_list[0][1], token_headers, token_request_data
+        )
+        # Verify service account impersonation request parameters.
+        self.assert_impersonation_request_kwargs(
+            request.call_args_list[1][1],
+            impersonation_headers,
+            impersonation_request_data,
+        )
+        assert credentials.valid
+        assert credentials.expiry == expected_expiry
+        assert not credentials.expired
+        assert credentials.token == impersonation_response["accessToken"]
+
+    def test_apply_without_quota_project_id(self):
+        headers = {}
+        request = self.make_mock_request(
+            status=http_client.OK, data=self.SUCCESS_RESPONSE
+        )
+        credentials = self.make_credentials()
+
+        credentials.refresh(request)
+        credentials.apply(headers)
+
+        assert headers == {
+            "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"])
+        }
+
+    def test_apply_workforce_without_quota_project_id(self):
+        headers = {}
+        request = self.make_mock_request(
+            status=http_client.OK, data=self.SUCCESS_RESPONSE
+        )
+        credentials = self.make_workforce_pool_credentials(
+            workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT
+        )
+
+        credentials.refresh(request)
+        credentials.apply(headers)
+
+        assert headers == {
+            "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"])
+        }
+
+    def test_apply_impersonation_without_quota_project_id(self):
+        expire_time = (
+            _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=3600)
+        ).isoformat("T") + "Z"
+        # Service account impersonation response.
+        impersonation_response = {
+            "accessToken": "SA_ACCESS_TOKEN",
+            "expireTime": expire_time,
+        }
+        # Initialize mock request to handle token exchange and service account
+        # impersonation request.
+        request = self.make_mock_request(
+            status=http_client.OK,
+            data=self.SUCCESS_RESPONSE.copy(),
+            impersonation_status=http_client.OK,
+            impersonation_data=impersonation_response,
+        )
+        # Initialize credentials with service account impersonation.
+        credentials = self.make_credentials(
+            service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL,
+            scopes=self.SCOPES,
+        )
+        headers = {}
+
+        credentials.refresh(request)
+        credentials.apply(headers)
+
+        assert headers == {
+            "authorization": "Bearer {}".format(impersonation_response["accessToken"])
+        }
+
+    def test_apply_with_quota_project_id(self):
+        headers = {"other": "header-value"}
+        request = self.make_mock_request(
+            status=http_client.OK, data=self.SUCCESS_RESPONSE
+        )
+        credentials = self.make_credentials(quota_project_id=self.QUOTA_PROJECT_ID)
+
+        credentials.refresh(request)
+        credentials.apply(headers)
+
+        assert headers == {
+            "other": "header-value",
+            "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]),
+            "x-goog-user-project": self.QUOTA_PROJECT_ID,
+        }
+
+    def test_apply_impersonation_with_quota_project_id(self):
+        expire_time = (
+            _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=3600)
+        ).isoformat("T") + "Z"
+        # Service account impersonation response.
+        impersonation_response = {
+            "accessToken": "SA_ACCESS_TOKEN",
+            "expireTime": expire_time,
+        }
+        # Initialize mock request to handle token exchange and service account
+        # impersonation request.
+        request = self.make_mock_request(
+            status=http_client.OK,
+            data=self.SUCCESS_RESPONSE.copy(),
+            impersonation_status=http_client.OK,
+            impersonation_data=impersonation_response,
+        )
+        # Initialize credentials with service account impersonation.
+        credentials = self.make_credentials(
+            service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL,
+            scopes=self.SCOPES,
+            quota_project_id=self.QUOTA_PROJECT_ID,
+        )
+        headers = {"other": "header-value"}
+
+        credentials.refresh(request)
+        credentials.apply(headers)
+
+        assert headers == {
+            "other": "header-value",
+            "authorization": "Bearer {}".format(impersonation_response["accessToken"]),
+            "x-goog-user-project": self.QUOTA_PROJECT_ID,
+        }
+
+    def test_before_request(self):
+        headers = {"other": "header-value"}
+        request = self.make_mock_request(
+            status=http_client.OK, data=self.SUCCESS_RESPONSE
+        )
+        credentials = self.make_credentials()
+
+        # First call should call refresh, setting the token.
+        credentials.before_request(request, "POST", "https://example.com/api", headers)
+
+        assert headers == {
+            "other": "header-value",
+            "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]),
+        }
+
+        # Second call shouldn't call refresh.
+        credentials.before_request(request, "POST", "https://example.com/api", headers)
+
+        assert headers == {
+            "other": "header-value",
+            "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]),
+        }
+
+    def test_before_request_workforce(self):
+        headers = {"other": "header-value"}
+        request = self.make_mock_request(
+            status=http_client.OK, data=self.SUCCESS_RESPONSE
+        )
+        credentials = self.make_workforce_pool_credentials(
+            workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT
+        )
+
+        # First call should call refresh, setting the token.
+        credentials.before_request(request, "POST", "https://example.com/api", headers)
+
+        assert headers == {
+            "other": "header-value",
+            "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]),
+        }
+
+        # Second call shouldn't call refresh.
+        credentials.before_request(request, "POST", "https://example.com/api", headers)
+
+        assert headers == {
+            "other": "header-value",
+            "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]),
+        }
+
+    def test_before_request_impersonation(self):
+        expire_time = (
+            _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=3600)
+        ).isoformat("T") + "Z"
+        # Service account impersonation response.
+        impersonation_response = {
+            "accessToken": "SA_ACCESS_TOKEN",
+            "expireTime": expire_time,
+        }
+        # Initialize mock request to handle token exchange and service account
+        # impersonation request.
+        request = self.make_mock_request(
+            status=http_client.OK,
+            data=self.SUCCESS_RESPONSE.copy(),
+            impersonation_status=http_client.OK,
+            impersonation_data=impersonation_response,
+        )
+        headers = {"other": "header-value"}
+        credentials = self.make_credentials(
+            service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL
+        )
+
+        # First call should call refresh, setting the token.
+        credentials.before_request(request, "POST", "https://example.com/api", headers)
+
+        assert headers == {
+            "other": "header-value",
+            "authorization": "Bearer {}".format(impersonation_response["accessToken"]),
+        }
+
+        # Second call shouldn't call refresh.
+        credentials.before_request(request, "POST", "https://example.com/api", headers)
+
+        assert headers == {
+            "other": "header-value",
+            "authorization": "Bearer {}".format(impersonation_response["accessToken"]),
+        }
+
+    @mock.patch("google.auth._helpers.utcnow")
+    def test_before_request_expired(self, utcnow):
+        headers = {}
+        request = self.make_mock_request(
+            status=http_client.OK, data=self.SUCCESS_RESPONSE
+        )
+        credentials = self.make_credentials()
+        credentials.token = "token"
+        utcnow.return_value = datetime.datetime.min
+        # Set the expiration to one second more than now plus the clock skew
+        # accomodation. These credentials should be valid.
+        credentials.expiry = (
+            datetime.datetime.min
+            + _helpers.REFRESH_THRESHOLD
+            + datetime.timedelta(seconds=1)
+        )
+
+        assert credentials.valid
+        assert not credentials.expired
+
+        credentials.before_request(request, "POST", "https://example.com/api", headers)
+
+        # Cached token should be used.
+        assert headers == {"authorization": "Bearer token"}
+
+        # Next call should simulate 1 second passed.
+        utcnow.return_value = datetime.datetime.min + datetime.timedelta(seconds=1)
+
+        assert not credentials.valid
+        assert credentials.expired
+
+        credentials.before_request(request, "POST", "https://example.com/api", headers)
+
+        # New token should be retrieved.
+        assert headers == {
+            "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"])
+        }
+
+    @mock.patch("google.auth._helpers.utcnow")
+    def test_before_request_impersonation_expired(self, utcnow):
+        headers = {}
+        expire_time = (
+            datetime.datetime.min + datetime.timedelta(seconds=3601)
+        ).isoformat("T") + "Z"
+        # Service account impersonation response.
+        impersonation_response = {
+            "accessToken": "SA_ACCESS_TOKEN",
+            "expireTime": expire_time,
+        }
+        # Initialize mock request to handle token exchange and service account
+        # impersonation request.
+        request = self.make_mock_request(
+            status=http_client.OK,
+            data=self.SUCCESS_RESPONSE.copy(),
+            impersonation_status=http_client.OK,
+            impersonation_data=impersonation_response,
+        )
+        credentials = self.make_credentials(
+            service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL
+        )
+        credentials.token = "token"
+        utcnow.return_value = datetime.datetime.min
+        # Set the expiration to one second more than now plus the clock skew
+        # accomodation. These credentials should be valid.
+        credentials.expiry = (
+            datetime.datetime.min
+            + _helpers.REFRESH_THRESHOLD
+            + datetime.timedelta(seconds=1)
+        )
+
+        assert credentials.valid
+        assert not credentials.expired
+
+        credentials.before_request(request, "POST", "https://example.com/api", headers)
+
+        # Cached token should be used.
+        assert headers == {"authorization": "Bearer token"}
+
+        # Next call should simulate 1 second passed. This will trigger the expiration
+        # threshold.
+        utcnow.return_value = datetime.datetime.min + datetime.timedelta(seconds=1)
+
+        assert not credentials.valid
+        assert credentials.expired
+
+        credentials.before_request(request, "POST", "https://example.com/api", headers)
+
+        # New token should be retrieved.
+        assert headers == {
+            "authorization": "Bearer {}".format(impersonation_response["accessToken"])
+        }
+
+    @pytest.mark.parametrize(
+        "audience",
+        [
+            # Legacy K8s audience format.
+            "identitynamespace:1f12345:my_provider",
+            # Unrealistic audiences.
+            "//iam.googleapis.com/projects",
+            "//iam.googleapis.com/projects/",
+            "//iam.googleapis.com/project/123456",
+            "//iam.googleapis.com/projects//123456",
+            "//iam.googleapis.com/prefix_projects/123456",
+            "//iam.googleapis.com/projects_suffix/123456",
+        ],
+    )
+    def test_project_number_indeterminable(self, audience):
+        credentials = CredentialsImpl(
+            audience=audience,
+            subject_token_type=self.SUBJECT_TOKEN_TYPE,
+            token_url=self.TOKEN_URL,
+            credential_source=self.CREDENTIAL_SOURCE,
+        )
+
+        assert credentials.project_number is None
+        assert credentials.get_project_id(None) is None
+
+    def test_project_number_determinable(self):
+        credentials = CredentialsImpl(
+            audience=self.AUDIENCE,
+            subject_token_type=self.SUBJECT_TOKEN_TYPE,
+            token_url=self.TOKEN_URL,
+            credential_source=self.CREDENTIAL_SOURCE,
+        )
+
+        assert credentials.project_number == self.PROJECT_NUMBER
+
+    def test_project_number_workforce(self):
+        credentials = CredentialsImpl(
+            audience=self.WORKFORCE_AUDIENCE,
+            subject_token_type=self.WORKFORCE_SUBJECT_TOKEN_TYPE,
+            token_url=self.TOKEN_URL,
+            credential_source=self.CREDENTIAL_SOURCE,
+            workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT,
+        )
+
+        assert credentials.project_number is None
+
+    def test_project_id_without_scopes(self):
+        # Initialize credentials with no scopes.
+        credentials = CredentialsImpl(
+            audience=self.AUDIENCE,
+            subject_token_type=self.SUBJECT_TOKEN_TYPE,
+            token_url=self.TOKEN_URL,
+            credential_source=self.CREDENTIAL_SOURCE,
+        )
+
+        assert credentials.get_project_id(None) is None
+
+    def test_get_project_id_cloud_resource_manager_success(self):
+        # STS token exchange request/response.
+        token_response = self.SUCCESS_RESPONSE.copy()
+        token_headers = {"Content-Type": "application/x-www-form-urlencoded"}
+        token_request_data = {
+            "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
+            "audience": self.AUDIENCE,
+            "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
+            "subject_token": "subject_token_0",
+            "subject_token_type": self.SUBJECT_TOKEN_TYPE,
+            "scope": "https://www.googleapis.com/auth/iam",
+        }
+        # Service account impersonation request/response.
+        expire_time = (
+            _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=3600)
+        ).isoformat("T") + "Z"
+        expected_expiry = datetime.datetime.strptime(expire_time, "%Y-%m-%dT%H:%M:%SZ")
+        impersonation_response = {
+            "accessToken": "SA_ACCESS_TOKEN",
+            "expireTime": expire_time,
+        }
+        impersonation_headers = {
+            "Content-Type": "application/json",
+            "x-goog-user-project": self.QUOTA_PROJECT_ID,
+            "authorization": "Bearer {}".format(token_response["access_token"]),
+        }
+        impersonation_request_data = {
+            "delegates": None,
+            "scope": self.SCOPES,
+            "lifetime": "3600s",
+        }
+        # Initialize mock request to handle token exchange, service account
+        # impersonation and cloud resource manager request.
+        request = self.make_mock_request(
+            status=http_client.OK,
+            data=self.SUCCESS_RESPONSE.copy(),
+            impersonation_status=http_client.OK,
+            impersonation_data=impersonation_response,
+            cloud_resource_manager_status=http_client.OK,
+            cloud_resource_manager_data=self.CLOUD_RESOURCE_MANAGER_SUCCESS_RESPONSE,
+        )
+        credentials = self.make_credentials(
+            service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL,
+            scopes=self.SCOPES,
+            quota_project_id=self.QUOTA_PROJECT_ID,
+        )
+
+        # Expected project ID from cloud resource manager response should be returned.
+        project_id = credentials.get_project_id(request)
+
+        assert project_id == self.PROJECT_ID
+        # 3 requests should be processed.
+        assert len(request.call_args_list) == 3
+        # Verify token exchange request parameters.
+        self.assert_token_request_kwargs(
+            request.call_args_list[0][1], token_headers, token_request_data
+        )
+        # Verify service account impersonation request parameters.
+        self.assert_impersonation_request_kwargs(
+            request.call_args_list[1][1],
+            impersonation_headers,
+            impersonation_request_data,
+        )
+        # In the process of getting project ID, an access token should be
+        # retrieved.
+        assert credentials.valid
+        assert credentials.expiry == expected_expiry
+        assert not credentials.expired
+        assert credentials.token == impersonation_response["accessToken"]
+        # Verify cloud resource manager request parameters.
+        self.assert_resource_manager_request_kwargs(
+            request.call_args_list[2][1],
+            self.PROJECT_NUMBER,
+            {
+                "x-goog-user-project": self.QUOTA_PROJECT_ID,
+                "authorization": "Bearer {}".format(
+                    impersonation_response["accessToken"]
+                ),
+            },
+        )
+
+        # Calling get_project_id again should return the cached project_id.
+        project_id = credentials.get_project_id(request)
+
+        assert project_id == self.PROJECT_ID
+        # No additional requests.
+        assert len(request.call_args_list) == 3
+
+    def test_workforce_pool_get_project_id_cloud_resource_manager_success(self):
+        # STS token exchange request/response.
+        token_headers = {"Content-Type": "application/x-www-form-urlencoded"}
+        token_request_data = {
+            "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
+            "audience": self.WORKFORCE_AUDIENCE,
+            "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
+            "subject_token": "subject_token_0",
+            "subject_token_type": self.WORKFORCE_SUBJECT_TOKEN_TYPE,
+            "scope": "scope1 scope2",
+            "options": urllib.parse.quote(
+                json.dumps({"userProject": self.WORKFORCE_POOL_USER_PROJECT})
+            ),
+        }
+        # Initialize mock request to handle token exchange and cloud resource
+        # manager request.
+        request = self.make_mock_request(
+            status=http_client.OK,
+            data=self.SUCCESS_RESPONSE.copy(),
+            cloud_resource_manager_status=http_client.OK,
+            cloud_resource_manager_data=self.CLOUD_RESOURCE_MANAGER_SUCCESS_RESPONSE,
+        )
+        credentials = self.make_workforce_pool_credentials(
+            scopes=self.SCOPES,
+            quota_project_id=self.QUOTA_PROJECT_ID,
+            workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT,
+        )
+
+        # Expected project ID from cloud resource manager response should be returned.
+        project_id = credentials.get_project_id(request)
+
+        assert project_id == self.PROJECT_ID
+        # 2 requests should be processed.
+        assert len(request.call_args_list) == 2
+        # Verify token exchange request parameters.
+        self.assert_token_request_kwargs(
+            request.call_args_list[0][1], token_headers, token_request_data
+        )
+        # In the process of getting project ID, an access token should be
+        # retrieved.
+        assert credentials.valid
+        assert not credentials.expired
+        assert credentials.token == self.SUCCESS_RESPONSE["access_token"]
+        # Verify cloud resource manager request parameters.
+        self.assert_resource_manager_request_kwargs(
+            request.call_args_list[1][1],
+            self.WORKFORCE_POOL_USER_PROJECT,
+            {
+                "x-goog-user-project": self.QUOTA_PROJECT_ID,
+                "authorization": "Bearer {}".format(
+                    self.SUCCESS_RESPONSE["access_token"]
+                ),
+            },
+        )
+
+        # Calling get_project_id again should return the cached project_id.
+        project_id = credentials.get_project_id(request)
+
+        assert project_id == self.PROJECT_ID
+        # No additional requests.
+        assert len(request.call_args_list) == 2
+
+    def test_get_project_id_cloud_resource_manager_error(self):
+        # Simulate resource doesn't have sufficient permissions to access
+        # cloud resource manager.
+        request = self.make_mock_request(
+            status=http_client.OK,
+            data=self.SUCCESS_RESPONSE.copy(),
+            cloud_resource_manager_status=http_client.UNAUTHORIZED,
+        )
+        credentials = self.make_credentials(scopes=self.SCOPES)
+
+        project_id = credentials.get_project_id(request)
+
+        assert project_id is None
+        # Only 2 requests to STS and cloud resource manager should be sent.
+        assert len(request.call_args_list) == 2
diff --git a/tests/test_iam.py b/tests/test_iam.py
new file mode 100644
index 0000000..bc71225
--- /dev/null
+++ b/tests/test_iam.py
@@ -0,0 +1,102 @@
+# Copyright 2017 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 base64
+import datetime
+import json
+
+import mock
+import pytest
+from six.moves import http_client
+
+from google.auth import _helpers
+from google.auth import exceptions
+from google.auth import iam
+from google.auth import transport
+import google.auth.credentials
+
+
+def make_request(status, data=None):
+    response = mock.create_autospec(transport.Response, instance=True)
+    response.status = status
+
+    if data is not None:
+        response.data = json.dumps(data).encode("utf-8")
+
+    request = mock.create_autospec(transport.Request)
+    request.return_value = response
+    return request
+
+
+def make_credentials():
+    class CredentialsImpl(google.auth.credentials.Credentials):
+        def __init__(self):
+            super(CredentialsImpl, self).__init__()
+            self.token = "token"
+            # Force refresh
+            self.expiry = datetime.datetime.min + _helpers.REFRESH_THRESHOLD
+
+        def refresh(self, request):
+            pass
+
+        def with_quota_project(self, quota_project_id):
+            raise NotImplementedError()
+
+    return CredentialsImpl()
+
+
+class TestSigner(object):
+    def test_constructor(self):
+        request = mock.sentinel.request
+        credentials = mock.create_autospec(
+            google.auth.credentials.Credentials, instance=True
+        )
+
+        signer = iam.Signer(request, credentials, mock.sentinel.service_account_email)
+
+        assert signer._request == mock.sentinel.request
+        assert signer._credentials == credentials
+        assert signer._service_account_email == mock.sentinel.service_account_email
+
+    def test_key_id(self):
+        signer = iam.Signer(
+            mock.sentinel.request,
+            mock.sentinel.credentials,
+            mock.sentinel.service_account_email,
+        )
+
+        assert signer.key_id is None
+
+    def test_sign_bytes(self):
+        signature = b"DEADBEEF"
+        encoded_signature = base64.b64encode(signature).decode("utf-8")
+        request = make_request(http_client.OK, data={"signedBlob": encoded_signature})
+        credentials = make_credentials()
+
+        signer = iam.Signer(request, credentials, mock.sentinel.service_account_email)
+
+        returned_signature = signer.sign("123")
+
+        assert returned_signature == signature
+        kwargs = request.call_args[1]
+        assert kwargs["headers"]["Content-Type"] == "application/json"
+
+    def test_sign_bytes_failure(self):
+        request = make_request(http_client.UNAUTHORIZED)
+        credentials = make_credentials()
+
+        signer = iam.Signer(request, credentials, mock.sentinel.service_account_email)
+
+        with pytest.raises(exceptions.TransportError):
+            signer.sign("123")
diff --git a/tests/test_identity_pool.py b/tests/test_identity_pool.py
new file mode 100644
index 0000000..87e343b
--- /dev/null
+++ b/tests/test_identity_pool.py
@@ -0,0 +1,1108 @@
+# 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.
+
+import datetime
+import json
+import os
+
+import mock
+import pytest
+from six.moves import http_client
+from six.moves import urllib
+
+from google.auth import _helpers
+from google.auth import exceptions
+from google.auth import identity_pool
+from google.auth import transport
+
+
+CLIENT_ID = "username"
+CLIENT_SECRET = "password"
+# Base64 encoding of "username:password".
+BASIC_AUTH_ENCODING = "dXNlcm5hbWU6cGFzc3dvcmQ="
+SERVICE_ACCOUNT_EMAIL = "[email protected]"
+SERVICE_ACCOUNT_IMPERSONATION_URL = (
+    "https://us-east1-iamcredentials.googleapis.com/v1/projects/-"
+    + "/serviceAccounts/{}:generateAccessToken".format(SERVICE_ACCOUNT_EMAIL)
+)
+QUOTA_PROJECT_ID = "QUOTA_PROJECT_ID"
+SCOPES = ["scope1", "scope2"]
+DATA_DIR = os.path.join(os.path.dirname(__file__), "data")
+SUBJECT_TOKEN_TEXT_FILE = os.path.join(DATA_DIR, "external_subject_token.txt")
+SUBJECT_TOKEN_JSON_FILE = os.path.join(DATA_DIR, "external_subject_token.json")
+SUBJECT_TOKEN_FIELD_NAME = "access_token"
+
+with open(SUBJECT_TOKEN_TEXT_FILE) as fh:
+    TEXT_FILE_SUBJECT_TOKEN = fh.read()
+
+with open(SUBJECT_TOKEN_JSON_FILE) as fh:
+    JSON_FILE_CONTENT = json.load(fh)
+    JSON_FILE_SUBJECT_TOKEN = JSON_FILE_CONTENT.get(SUBJECT_TOKEN_FIELD_NAME)
+
+TOKEN_URL = "https://sts.googleapis.com/v1/token"
+SUBJECT_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:jwt"
+AUDIENCE = "//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID"
+WORKFORCE_AUDIENCE = (
+    "//iam.googleapis.com/locations/global/workforcePools/POOL_ID/providers/PROVIDER_ID"
+)
+WORKFORCE_SUBJECT_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:id_token"
+WORKFORCE_POOL_USER_PROJECT = "WORKFORCE_POOL_USER_PROJECT_NUMBER"
+
+
+class TestCredentials(object):
+    CREDENTIAL_SOURCE_TEXT = {"file": SUBJECT_TOKEN_TEXT_FILE}
+    CREDENTIAL_SOURCE_JSON = {
+        "file": SUBJECT_TOKEN_JSON_FILE,
+        "format": {"type": "json", "subject_token_field_name": "access_token"},
+    }
+    CREDENTIAL_URL = "http://fakeurl.com"
+    CREDENTIAL_SOURCE_TEXT_URL = {"url": CREDENTIAL_URL}
+    CREDENTIAL_SOURCE_JSON_URL = {
+        "url": CREDENTIAL_URL,
+        "format": {"type": "json", "subject_token_field_name": "access_token"},
+    }
+    SUCCESS_RESPONSE = {
+        "access_token": "ACCESS_TOKEN",
+        "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
+        "token_type": "Bearer",
+        "expires_in": 3600,
+        "scope": " ".join(SCOPES),
+    }
+
+    @classmethod
+    def make_mock_response(cls, status, data):
+        response = mock.create_autospec(transport.Response, instance=True)
+        response.status = status
+        if isinstance(data, dict):
+            response.data = json.dumps(data).encode("utf-8")
+        else:
+            response.data = data
+        return response
+
+    @classmethod
+    def make_mock_request(
+        cls, token_status=http_client.OK, token_data=None, *extra_requests
+    ):
+        responses = []
+        responses.append(cls.make_mock_response(token_status, token_data))
+
+        while len(extra_requests) > 0:
+            # If service account impersonation is requested, mock the expected response.
+            status, data, extra_requests = (
+                extra_requests[0],
+                extra_requests[1],
+                extra_requests[2:],
+            )
+            responses.append(cls.make_mock_response(status, data))
+
+        request = mock.create_autospec(transport.Request)
+        request.side_effect = responses
+
+        return request
+
+    @classmethod
+    def assert_credential_request_kwargs(
+        cls, request_kwargs, headers, url=CREDENTIAL_URL
+    ):
+        assert request_kwargs["url"] == url
+        assert request_kwargs["method"] == "GET"
+        assert request_kwargs["headers"] == headers
+        assert request_kwargs.get("body", None) is None
+
+    @classmethod
+    def assert_token_request_kwargs(
+        cls, request_kwargs, headers, request_data, token_url=TOKEN_URL
+    ):
+        assert request_kwargs["url"] == token_url
+        assert request_kwargs["method"] == "POST"
+        assert request_kwargs["headers"] == headers
+        assert request_kwargs["body"] is not None
+        body_tuples = urllib.parse.parse_qsl(request_kwargs["body"])
+        assert len(body_tuples) == len(request_data.keys())
+        for (k, v) in body_tuples:
+            assert v.decode("utf-8") == request_data[k.decode("utf-8")]
+
+    @classmethod
+    def assert_impersonation_request_kwargs(
+        cls,
+        request_kwargs,
+        headers,
+        request_data,
+        service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+    ):
+        assert request_kwargs["url"] == service_account_impersonation_url
+        assert request_kwargs["method"] == "POST"
+        assert request_kwargs["headers"] == headers
+        assert request_kwargs["body"] is not None
+        body_json = json.loads(request_kwargs["body"].decode("utf-8"))
+        assert body_json == request_data
+
+    @classmethod
+    def assert_underlying_credentials_refresh(
+        cls,
+        credentials,
+        audience,
+        subject_token,
+        subject_token_type,
+        token_url,
+        service_account_impersonation_url=None,
+        basic_auth_encoding=None,
+        quota_project_id=None,
+        used_scopes=None,
+        credential_data=None,
+        scopes=None,
+        default_scopes=None,
+        workforce_pool_user_project=None,
+    ):
+        """Utility to assert that a credentials are initialized with the expected
+        attributes by calling refresh functionality and confirming response matches
+        expected one and that the underlying requests were populated with the
+        expected parameters.
+        """
+        # STS token exchange request/response.
+        token_response = cls.SUCCESS_RESPONSE.copy()
+        token_headers = {"Content-Type": "application/x-www-form-urlencoded"}
+        if basic_auth_encoding:
+            token_headers["Authorization"] = "Basic " + basic_auth_encoding
+
+        if service_account_impersonation_url:
+            token_scopes = "https://www.googleapis.com/auth/iam"
+        else:
+            token_scopes = " ".join(used_scopes or [])
+
+        token_request_data = {
+            "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
+            "audience": audience,
+            "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
+            "scope": token_scopes,
+            "subject_token": subject_token,
+            "subject_token_type": subject_token_type,
+        }
+        if workforce_pool_user_project:
+            token_request_data["options"] = urllib.parse.quote(
+                json.dumps({"userProject": workforce_pool_user_project})
+            )
+
+        if service_account_impersonation_url:
+            # Service account impersonation request/response.
+            expire_time = (
+                _helpers.utcnow().replace(microsecond=0)
+                + datetime.timedelta(seconds=3600)
+            ).isoformat("T") + "Z"
+            impersonation_response = {
+                "accessToken": "SA_ACCESS_TOKEN",
+                "expireTime": expire_time,
+            }
+            impersonation_headers = {
+                "Content-Type": "application/json",
+                "authorization": "Bearer {}".format(token_response["access_token"]),
+            }
+            impersonation_request_data = {
+                "delegates": None,
+                "scope": used_scopes,
+                "lifetime": "3600s",
+            }
+
+        # Initialize mock request to handle token retrieval, token exchange and
+        # service account impersonation request.
+        requests = []
+        if credential_data:
+            requests.append((http_client.OK, credential_data))
+
+        token_request_index = len(requests)
+        requests.append((http_client.OK, token_response))
+
+        if service_account_impersonation_url:
+            impersonation_request_index = len(requests)
+            requests.append((http_client.OK, impersonation_response))
+
+        request = cls.make_mock_request(*[el for req in requests for el in req])
+
+        credentials.refresh(request)
+
+        assert len(request.call_args_list) == len(requests)
+        if credential_data:
+            cls.assert_credential_request_kwargs(request.call_args_list[0][1], None)
+        # Verify token exchange request parameters.
+        cls.assert_token_request_kwargs(
+            request.call_args_list[token_request_index][1],
+            token_headers,
+            token_request_data,
+            token_url,
+        )
+        # Verify service account impersonation request parameters if the request
+        # is processed.
+        if service_account_impersonation_url:
+            cls.assert_impersonation_request_kwargs(
+                request.call_args_list[impersonation_request_index][1],
+                impersonation_headers,
+                impersonation_request_data,
+                service_account_impersonation_url,
+            )
+            assert credentials.token == impersonation_response["accessToken"]
+        else:
+            assert credentials.token == token_response["access_token"]
+        assert credentials.quota_project_id == quota_project_id
+        assert credentials.scopes == scopes
+        assert credentials.default_scopes == default_scopes
+
+    @classmethod
+    def make_credentials(
+        cls,
+        audience=AUDIENCE,
+        subject_token_type=SUBJECT_TOKEN_TYPE,
+        client_id=None,
+        client_secret=None,
+        quota_project_id=None,
+        scopes=None,
+        default_scopes=None,
+        service_account_impersonation_url=None,
+        credential_source=None,
+        workforce_pool_user_project=None,
+    ):
+        return identity_pool.Credentials(
+            audience=audience,
+            subject_token_type=subject_token_type,
+            token_url=TOKEN_URL,
+            service_account_impersonation_url=service_account_impersonation_url,
+            credential_source=credential_source,
+            client_id=client_id,
+            client_secret=client_secret,
+            quota_project_id=quota_project_id,
+            scopes=scopes,
+            default_scopes=default_scopes,
+            workforce_pool_user_project=workforce_pool_user_project,
+        )
+
+    @mock.patch.object(identity_pool.Credentials, "__init__", return_value=None)
+    def test_from_info_full_options(self, mock_init):
+        credentials = identity_pool.Credentials.from_info(
+            {
+                "audience": AUDIENCE,
+                "subject_token_type": SUBJECT_TOKEN_TYPE,
+                "token_url": TOKEN_URL,
+                "service_account_impersonation_url": SERVICE_ACCOUNT_IMPERSONATION_URL,
+                "client_id": CLIENT_ID,
+                "client_secret": CLIENT_SECRET,
+                "quota_project_id": QUOTA_PROJECT_ID,
+                "credential_source": self.CREDENTIAL_SOURCE_TEXT,
+            }
+        )
+
+        # Confirm identity_pool.Credentials instantiated with expected attributes.
+        assert isinstance(credentials, identity_pool.Credentials)
+        mock_init.assert_called_once_with(
+            audience=AUDIENCE,
+            subject_token_type=SUBJECT_TOKEN_TYPE,
+            token_url=TOKEN_URL,
+            service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+            client_id=CLIENT_ID,
+            client_secret=CLIENT_SECRET,
+            credential_source=self.CREDENTIAL_SOURCE_TEXT,
+            quota_project_id=QUOTA_PROJECT_ID,
+            workforce_pool_user_project=None,
+        )
+
+    @mock.patch.object(identity_pool.Credentials, "__init__", return_value=None)
+    def test_from_info_required_options_only(self, mock_init):
+        credentials = identity_pool.Credentials.from_info(
+            {
+                "audience": AUDIENCE,
+                "subject_token_type": SUBJECT_TOKEN_TYPE,
+                "token_url": TOKEN_URL,
+                "credential_source": self.CREDENTIAL_SOURCE_TEXT,
+            }
+        )
+
+        # Confirm identity_pool.Credentials instantiated with expected attributes.
+        assert isinstance(credentials, identity_pool.Credentials)
+        mock_init.assert_called_once_with(
+            audience=AUDIENCE,
+            subject_token_type=SUBJECT_TOKEN_TYPE,
+            token_url=TOKEN_URL,
+            service_account_impersonation_url=None,
+            client_id=None,
+            client_secret=None,
+            credential_source=self.CREDENTIAL_SOURCE_TEXT,
+            quota_project_id=None,
+            workforce_pool_user_project=None,
+        )
+
+    @mock.patch.object(identity_pool.Credentials, "__init__", return_value=None)
+    def test_from_info_workforce_pool(self, mock_init):
+        credentials = identity_pool.Credentials.from_info(
+            {
+                "audience": WORKFORCE_AUDIENCE,
+                "subject_token_type": WORKFORCE_SUBJECT_TOKEN_TYPE,
+                "token_url": TOKEN_URL,
+                "credential_source": self.CREDENTIAL_SOURCE_TEXT,
+                "workforce_pool_user_project": WORKFORCE_POOL_USER_PROJECT,
+            }
+        )
+
+        # Confirm identity_pool.Credentials instantiated with expected attributes.
+        assert isinstance(credentials, identity_pool.Credentials)
+        mock_init.assert_called_once_with(
+            audience=WORKFORCE_AUDIENCE,
+            subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE,
+            token_url=TOKEN_URL,
+            service_account_impersonation_url=None,
+            client_id=None,
+            client_secret=None,
+            credential_source=self.CREDENTIAL_SOURCE_TEXT,
+            quota_project_id=None,
+            workforce_pool_user_project=WORKFORCE_POOL_USER_PROJECT,
+        )
+
+    @mock.patch.object(identity_pool.Credentials, "__init__", return_value=None)
+    def test_from_file_full_options(self, mock_init, tmpdir):
+        info = {
+            "audience": AUDIENCE,
+            "subject_token_type": SUBJECT_TOKEN_TYPE,
+            "token_url": TOKEN_URL,
+            "service_account_impersonation_url": SERVICE_ACCOUNT_IMPERSONATION_URL,
+            "client_id": CLIENT_ID,
+            "client_secret": CLIENT_SECRET,
+            "quota_project_id": QUOTA_PROJECT_ID,
+            "credential_source": self.CREDENTIAL_SOURCE_TEXT,
+        }
+        config_file = tmpdir.join("config.json")
+        config_file.write(json.dumps(info))
+        credentials = identity_pool.Credentials.from_file(str(config_file))
+
+        # Confirm identity_pool.Credentials instantiated with expected attributes.
+        assert isinstance(credentials, identity_pool.Credentials)
+        mock_init.assert_called_once_with(
+            audience=AUDIENCE,
+            subject_token_type=SUBJECT_TOKEN_TYPE,
+            token_url=TOKEN_URL,
+            service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+            client_id=CLIENT_ID,
+            client_secret=CLIENT_SECRET,
+            credential_source=self.CREDENTIAL_SOURCE_TEXT,
+            quota_project_id=QUOTA_PROJECT_ID,
+            workforce_pool_user_project=None,
+        )
+
+    @mock.patch.object(identity_pool.Credentials, "__init__", return_value=None)
+    def test_from_file_required_options_only(self, mock_init, tmpdir):
+        info = {
+            "audience": AUDIENCE,
+            "subject_token_type": SUBJECT_TOKEN_TYPE,
+            "token_url": TOKEN_URL,
+            "credential_source": self.CREDENTIAL_SOURCE_TEXT,
+        }
+        config_file = tmpdir.join("config.json")
+        config_file.write(json.dumps(info))
+        credentials = identity_pool.Credentials.from_file(str(config_file))
+
+        # Confirm identity_pool.Credentials instantiated with expected attributes.
+        assert isinstance(credentials, identity_pool.Credentials)
+        mock_init.assert_called_once_with(
+            audience=AUDIENCE,
+            subject_token_type=SUBJECT_TOKEN_TYPE,
+            token_url=TOKEN_URL,
+            service_account_impersonation_url=None,
+            client_id=None,
+            client_secret=None,
+            credential_source=self.CREDENTIAL_SOURCE_TEXT,
+            quota_project_id=None,
+            workforce_pool_user_project=None,
+        )
+
+    @mock.patch.object(identity_pool.Credentials, "__init__", return_value=None)
+    def test_from_file_workforce_pool(self, mock_init, tmpdir):
+        info = {
+            "audience": WORKFORCE_AUDIENCE,
+            "subject_token_type": WORKFORCE_SUBJECT_TOKEN_TYPE,
+            "token_url": TOKEN_URL,
+            "credential_source": self.CREDENTIAL_SOURCE_TEXT,
+            "workforce_pool_user_project": WORKFORCE_POOL_USER_PROJECT,
+        }
+        config_file = tmpdir.join("config.json")
+        config_file.write(json.dumps(info))
+        credentials = identity_pool.Credentials.from_file(str(config_file))
+
+        # Confirm identity_pool.Credentials instantiated with expected attributes.
+        assert isinstance(credentials, identity_pool.Credentials)
+        mock_init.assert_called_once_with(
+            audience=WORKFORCE_AUDIENCE,
+            subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE,
+            token_url=TOKEN_URL,
+            service_account_impersonation_url=None,
+            client_id=None,
+            client_secret=None,
+            credential_source=self.CREDENTIAL_SOURCE_TEXT,
+            quota_project_id=None,
+            workforce_pool_user_project=WORKFORCE_POOL_USER_PROJECT,
+        )
+
+    def test_constructor_nonworkforce_with_workforce_pool_user_project(self):
+        with pytest.raises(ValueError) as excinfo:
+            self.make_credentials(
+                audience=AUDIENCE,
+                workforce_pool_user_project=WORKFORCE_POOL_USER_PROJECT,
+            )
+
+        assert excinfo.match(
+            "workforce_pool_user_project should not be set for non-workforce "
+            "pool credentials"
+        )
+
+    def test_constructor_invalid_options(self):
+        credential_source = {"unsupported": "value"}
+
+        with pytest.raises(ValueError) as excinfo:
+            self.make_credentials(credential_source=credential_source)
+
+        assert excinfo.match(r"Missing credential_source")
+
+    def test_constructor_invalid_options_url_and_file(self):
+        credential_source = {
+            "url": self.CREDENTIAL_URL,
+            "file": SUBJECT_TOKEN_TEXT_FILE,
+        }
+
+        with pytest.raises(ValueError) as excinfo:
+            self.make_credentials(credential_source=credential_source)
+
+        assert excinfo.match(r"Ambiguous credential_source")
+
+    def test_constructor_invalid_options_environment_id(self):
+        credential_source = {"url": self.CREDENTIAL_URL, "environment_id": "aws1"}
+
+        with pytest.raises(ValueError) as excinfo:
+            self.make_credentials(credential_source=credential_source)
+
+        assert excinfo.match(
+            r"Invalid Identity Pool credential_source field 'environment_id'"
+        )
+
+    def test_constructor_invalid_credential_source(self):
+        with pytest.raises(ValueError) as excinfo:
+            self.make_credentials(credential_source="non-dict")
+
+        assert excinfo.match(r"Missing credential_source")
+
+    def test_constructor_invalid_credential_source_format_type(self):
+        credential_source = {"format": {"type": "xml"}}
+
+        with pytest.raises(ValueError) as excinfo:
+            self.make_credentials(credential_source=credential_source)
+
+        assert excinfo.match(r"Invalid credential_source format 'xml'")
+
+    def test_constructor_missing_subject_token_field_name(self):
+        credential_source = {"format": {"type": "json"}}
+
+        with pytest.raises(ValueError) as excinfo:
+            self.make_credentials(credential_source=credential_source)
+
+        assert excinfo.match(
+            r"Missing subject_token_field_name for JSON credential_source format"
+        )
+
+    def test_info_with_workforce_pool_user_project(self):
+        credentials = self.make_credentials(
+            audience=WORKFORCE_AUDIENCE,
+            subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE,
+            credential_source=self.CREDENTIAL_SOURCE_TEXT_URL.copy(),
+            workforce_pool_user_project=WORKFORCE_POOL_USER_PROJECT,
+        )
+
+        assert credentials.info == {
+            "type": "external_account",
+            "audience": WORKFORCE_AUDIENCE,
+            "subject_token_type": WORKFORCE_SUBJECT_TOKEN_TYPE,
+            "token_url": TOKEN_URL,
+            "credential_source": self.CREDENTIAL_SOURCE_TEXT_URL,
+            "workforce_pool_user_project": WORKFORCE_POOL_USER_PROJECT,
+        }
+
+    def test_info_with_file_credential_source(self):
+        credentials = self.make_credentials(
+            credential_source=self.CREDENTIAL_SOURCE_TEXT_URL.copy()
+        )
+
+        assert credentials.info == {
+            "type": "external_account",
+            "audience": AUDIENCE,
+            "subject_token_type": SUBJECT_TOKEN_TYPE,
+            "token_url": TOKEN_URL,
+            "credential_source": self.CREDENTIAL_SOURCE_TEXT_URL,
+        }
+
+    def test_info_with_url_credential_source(self):
+        credentials = self.make_credentials(
+            credential_source=self.CREDENTIAL_SOURCE_JSON_URL.copy()
+        )
+
+        assert credentials.info == {
+            "type": "external_account",
+            "audience": AUDIENCE,
+            "subject_token_type": SUBJECT_TOKEN_TYPE,
+            "token_url": TOKEN_URL,
+            "credential_source": self.CREDENTIAL_SOURCE_JSON_URL,
+        }
+
+    def test_retrieve_subject_token_missing_subject_token(self, tmpdir):
+        # Provide empty text file.
+        empty_file = tmpdir.join("empty.txt")
+        empty_file.write("")
+        credential_source = {"file": str(empty_file)}
+        credentials = self.make_credentials(credential_source=credential_source)
+
+        with pytest.raises(exceptions.RefreshError) as excinfo:
+            credentials.retrieve_subject_token(None)
+
+        assert excinfo.match(r"Missing subject_token in the credential_source file")
+
+    def test_retrieve_subject_token_text_file(self):
+        credentials = self.make_credentials(
+            credential_source=self.CREDENTIAL_SOURCE_TEXT
+        )
+
+        subject_token = credentials.retrieve_subject_token(None)
+
+        assert subject_token == TEXT_FILE_SUBJECT_TOKEN
+
+    def test_retrieve_subject_token_json_file(self):
+        credentials = self.make_credentials(
+            credential_source=self.CREDENTIAL_SOURCE_JSON
+        )
+
+        subject_token = credentials.retrieve_subject_token(None)
+
+        assert subject_token == JSON_FILE_SUBJECT_TOKEN
+
+    def test_retrieve_subject_token_json_file_invalid_field_name(self):
+        credential_source = {
+            "file": SUBJECT_TOKEN_JSON_FILE,
+            "format": {"type": "json", "subject_token_field_name": "not_found"},
+        }
+        credentials = self.make_credentials(credential_source=credential_source)
+
+        with pytest.raises(exceptions.RefreshError) as excinfo:
+            credentials.retrieve_subject_token(None)
+
+        assert excinfo.match(
+            "Unable to parse subject_token from JSON file '{}' using key '{}'".format(
+                SUBJECT_TOKEN_JSON_FILE, "not_found"
+            )
+        )
+
+    def test_retrieve_subject_token_invalid_json(self, tmpdir):
+        # Provide JSON file. This should result in JSON parsing error.
+        invalid_json_file = tmpdir.join("invalid.json")
+        invalid_json_file.write("{")
+        credential_source = {
+            "file": str(invalid_json_file),
+            "format": {"type": "json", "subject_token_field_name": "access_token"},
+        }
+        credentials = self.make_credentials(credential_source=credential_source)
+
+        with pytest.raises(exceptions.RefreshError) as excinfo:
+            credentials.retrieve_subject_token(None)
+
+        assert excinfo.match(
+            "Unable to parse subject_token from JSON file '{}' using key '{}'".format(
+                str(invalid_json_file), "access_token"
+            )
+        )
+
+    def test_retrieve_subject_token_file_not_found(self):
+        credential_source = {"file": "./not_found.txt"}
+        credentials = self.make_credentials(credential_source=credential_source)
+
+        with pytest.raises(exceptions.RefreshError) as excinfo:
+            credentials.retrieve_subject_token(None)
+
+        assert excinfo.match(r"File './not_found.txt' was not found")
+
+    def test_refresh_text_file_success_without_impersonation_ignore_default_scopes(
+        self,
+    ):
+        credentials = self.make_credentials(
+            client_id=CLIENT_ID,
+            client_secret=CLIENT_SECRET,
+            # Test with text format type.
+            credential_source=self.CREDENTIAL_SOURCE_TEXT,
+            scopes=SCOPES,
+            # Default scopes should be ignored.
+            default_scopes=["ignored"],
+        )
+
+        self.assert_underlying_credentials_refresh(
+            credentials=credentials,
+            audience=AUDIENCE,
+            subject_token=TEXT_FILE_SUBJECT_TOKEN,
+            subject_token_type=SUBJECT_TOKEN_TYPE,
+            token_url=TOKEN_URL,
+            service_account_impersonation_url=None,
+            basic_auth_encoding=BASIC_AUTH_ENCODING,
+            quota_project_id=None,
+            used_scopes=SCOPES,
+            scopes=SCOPES,
+            default_scopes=["ignored"],
+        )
+
+    def test_refresh_workforce_success_with_client_auth_without_impersonation(self):
+        credentials = self.make_credentials(
+            audience=WORKFORCE_AUDIENCE,
+            subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE,
+            client_id=CLIENT_ID,
+            client_secret=CLIENT_SECRET,
+            # Test with text format type.
+            credential_source=self.CREDENTIAL_SOURCE_TEXT,
+            scopes=SCOPES,
+            # This will be ignored in favor of client auth.
+            workforce_pool_user_project=WORKFORCE_POOL_USER_PROJECT,
+        )
+
+        self.assert_underlying_credentials_refresh(
+            credentials=credentials,
+            audience=WORKFORCE_AUDIENCE,
+            subject_token=TEXT_FILE_SUBJECT_TOKEN,
+            subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE,
+            token_url=TOKEN_URL,
+            service_account_impersonation_url=None,
+            basic_auth_encoding=BASIC_AUTH_ENCODING,
+            quota_project_id=None,
+            used_scopes=SCOPES,
+            scopes=SCOPES,
+            workforce_pool_user_project=None,
+        )
+
+    def test_refresh_workforce_success_with_client_auth_and_no_workforce_project(self):
+        credentials = self.make_credentials(
+            audience=WORKFORCE_AUDIENCE,
+            subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE,
+            client_id=CLIENT_ID,
+            client_secret=CLIENT_SECRET,
+            # Test with text format type.
+            credential_source=self.CREDENTIAL_SOURCE_TEXT,
+            scopes=SCOPES,
+            # This is not needed when client Auth is used.
+            workforce_pool_user_project=None,
+        )
+
+        self.assert_underlying_credentials_refresh(
+            credentials=credentials,
+            audience=WORKFORCE_AUDIENCE,
+            subject_token=TEXT_FILE_SUBJECT_TOKEN,
+            subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE,
+            token_url=TOKEN_URL,
+            service_account_impersonation_url=None,
+            basic_auth_encoding=BASIC_AUTH_ENCODING,
+            quota_project_id=None,
+            used_scopes=SCOPES,
+            scopes=SCOPES,
+            workforce_pool_user_project=None,
+        )
+
+    def test_refresh_workforce_success_without_client_auth_without_impersonation(self):
+        credentials = self.make_credentials(
+            audience=WORKFORCE_AUDIENCE,
+            subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE,
+            client_id=None,
+            client_secret=None,
+            # Test with text format type.
+            credential_source=self.CREDENTIAL_SOURCE_TEXT,
+            scopes=SCOPES,
+            # This will not be ignored as client auth is not used.
+            workforce_pool_user_project=WORKFORCE_POOL_USER_PROJECT,
+        )
+
+        self.assert_underlying_credentials_refresh(
+            credentials=credentials,
+            audience=WORKFORCE_AUDIENCE,
+            subject_token=TEXT_FILE_SUBJECT_TOKEN,
+            subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE,
+            token_url=TOKEN_URL,
+            service_account_impersonation_url=None,
+            basic_auth_encoding=None,
+            quota_project_id=None,
+            used_scopes=SCOPES,
+            scopes=SCOPES,
+            workforce_pool_user_project=WORKFORCE_POOL_USER_PROJECT,
+        )
+
+    def test_refresh_workforce_success_without_client_auth_with_impersonation(self):
+        credentials = self.make_credentials(
+            audience=WORKFORCE_AUDIENCE,
+            subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE,
+            client_id=None,
+            client_secret=None,
+            service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+            # Test with text format type.
+            credential_source=self.CREDENTIAL_SOURCE_TEXT,
+            scopes=SCOPES,
+            # This will not be ignored as client auth is not used.
+            workforce_pool_user_project=WORKFORCE_POOL_USER_PROJECT,
+        )
+
+        self.assert_underlying_credentials_refresh(
+            credentials=credentials,
+            audience=WORKFORCE_AUDIENCE,
+            subject_token=TEXT_FILE_SUBJECT_TOKEN,
+            subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE,
+            token_url=TOKEN_URL,
+            service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+            basic_auth_encoding=None,
+            quota_project_id=None,
+            used_scopes=SCOPES,
+            scopes=SCOPES,
+            workforce_pool_user_project=WORKFORCE_POOL_USER_PROJECT,
+        )
+
+    def test_refresh_text_file_success_without_impersonation_use_default_scopes(self):
+        credentials = self.make_credentials(
+            client_id=CLIENT_ID,
+            client_secret=CLIENT_SECRET,
+            # Test with text format type.
+            credential_source=self.CREDENTIAL_SOURCE_TEXT,
+            scopes=None,
+            # Default scopes should be used since user specified scopes are none.
+            default_scopes=SCOPES,
+        )
+
+        self.assert_underlying_credentials_refresh(
+            credentials=credentials,
+            audience=AUDIENCE,
+            subject_token=TEXT_FILE_SUBJECT_TOKEN,
+            subject_token_type=SUBJECT_TOKEN_TYPE,
+            token_url=TOKEN_URL,
+            service_account_impersonation_url=None,
+            basic_auth_encoding=BASIC_AUTH_ENCODING,
+            quota_project_id=None,
+            used_scopes=SCOPES,
+            scopes=None,
+            default_scopes=SCOPES,
+        )
+
+    def test_refresh_text_file_success_with_impersonation_ignore_default_scopes(self):
+        # Initialize credentials with service account impersonation and basic auth.
+        credentials = self.make_credentials(
+            # Test with text format type.
+            credential_source=self.CREDENTIAL_SOURCE_TEXT,
+            service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+            scopes=SCOPES,
+            # Default scopes should be ignored.
+            default_scopes=["ignored"],
+        )
+
+        self.assert_underlying_credentials_refresh(
+            credentials=credentials,
+            audience=AUDIENCE,
+            subject_token=TEXT_FILE_SUBJECT_TOKEN,
+            subject_token_type=SUBJECT_TOKEN_TYPE,
+            token_url=TOKEN_URL,
+            service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+            basic_auth_encoding=None,
+            quota_project_id=None,
+            used_scopes=SCOPES,
+            scopes=SCOPES,
+            default_scopes=["ignored"],
+        )
+
+    def test_refresh_text_file_success_with_impersonation_use_default_scopes(self):
+        # Initialize credentials with service account impersonation, basic auth
+        # and default scopes (no user scopes).
+        credentials = self.make_credentials(
+            # Test with text format type.
+            credential_source=self.CREDENTIAL_SOURCE_TEXT,
+            service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+            scopes=None,
+            # Default scopes should be used since user specified scopes are none.
+            default_scopes=SCOPES,
+        )
+
+        self.assert_underlying_credentials_refresh(
+            credentials=credentials,
+            audience=AUDIENCE,
+            subject_token=TEXT_FILE_SUBJECT_TOKEN,
+            subject_token_type=SUBJECT_TOKEN_TYPE,
+            token_url=TOKEN_URL,
+            service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+            basic_auth_encoding=None,
+            quota_project_id=None,
+            used_scopes=SCOPES,
+            scopes=None,
+            default_scopes=SCOPES,
+        )
+
+    def test_refresh_json_file_success_without_impersonation(self):
+        credentials = self.make_credentials(
+            client_id=CLIENT_ID,
+            client_secret=CLIENT_SECRET,
+            # Test with JSON format type.
+            credential_source=self.CREDENTIAL_SOURCE_JSON,
+            scopes=SCOPES,
+        )
+
+        self.assert_underlying_credentials_refresh(
+            credentials=credentials,
+            audience=AUDIENCE,
+            subject_token=JSON_FILE_SUBJECT_TOKEN,
+            subject_token_type=SUBJECT_TOKEN_TYPE,
+            token_url=TOKEN_URL,
+            service_account_impersonation_url=None,
+            basic_auth_encoding=BASIC_AUTH_ENCODING,
+            quota_project_id=None,
+            used_scopes=SCOPES,
+            scopes=SCOPES,
+            default_scopes=None,
+        )
+
+    def test_refresh_json_file_success_with_impersonation(self):
+        # Initialize credentials with service account impersonation and basic auth.
+        credentials = self.make_credentials(
+            # Test with JSON format type.
+            credential_source=self.CREDENTIAL_SOURCE_JSON,
+            service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+            scopes=SCOPES,
+        )
+
+        self.assert_underlying_credentials_refresh(
+            credentials=credentials,
+            audience=AUDIENCE,
+            subject_token=JSON_FILE_SUBJECT_TOKEN,
+            subject_token_type=SUBJECT_TOKEN_TYPE,
+            token_url=TOKEN_URL,
+            service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+            basic_auth_encoding=None,
+            quota_project_id=None,
+            used_scopes=SCOPES,
+            scopes=SCOPES,
+            default_scopes=None,
+        )
+
+    def test_refresh_with_retrieve_subject_token_error(self):
+        credential_source = {
+            "file": SUBJECT_TOKEN_JSON_FILE,
+            "format": {"type": "json", "subject_token_field_name": "not_found"},
+        }
+        credentials = self.make_credentials(credential_source=credential_source)
+
+        with pytest.raises(exceptions.RefreshError) as excinfo:
+            credentials.refresh(None)
+
+        assert excinfo.match(
+            "Unable to parse subject_token from JSON file '{}' using key '{}'".format(
+                SUBJECT_TOKEN_JSON_FILE, "not_found"
+            )
+        )
+
+    def test_retrieve_subject_token_from_url(self):
+        credentials = self.make_credentials(
+            credential_source=self.CREDENTIAL_SOURCE_TEXT_URL
+        )
+        request = self.make_mock_request(token_data=TEXT_FILE_SUBJECT_TOKEN)
+        subject_token = credentials.retrieve_subject_token(request)
+
+        assert subject_token == TEXT_FILE_SUBJECT_TOKEN
+        self.assert_credential_request_kwargs(request.call_args_list[0][1], None)
+
+    def test_retrieve_subject_token_from_url_with_headers(self):
+        credentials = self.make_credentials(
+            credential_source={"url": self.CREDENTIAL_URL, "headers": {"foo": "bar"}}
+        )
+        request = self.make_mock_request(token_data=TEXT_FILE_SUBJECT_TOKEN)
+        subject_token = credentials.retrieve_subject_token(request)
+
+        assert subject_token == TEXT_FILE_SUBJECT_TOKEN
+        self.assert_credential_request_kwargs(
+            request.call_args_list[0][1], {"foo": "bar"}
+        )
+
+    def test_retrieve_subject_token_from_url_json(self):
+        credentials = self.make_credentials(
+            credential_source=self.CREDENTIAL_SOURCE_JSON_URL
+        )
+        request = self.make_mock_request(token_data=JSON_FILE_CONTENT)
+        subject_token = credentials.retrieve_subject_token(request)
+
+        assert subject_token == JSON_FILE_SUBJECT_TOKEN
+        self.assert_credential_request_kwargs(request.call_args_list[0][1], None)
+
+    def test_retrieve_subject_token_from_url_json_with_headers(self):
+        credentials = self.make_credentials(
+            credential_source={
+                "url": self.CREDENTIAL_URL,
+                "format": {"type": "json", "subject_token_field_name": "access_token"},
+                "headers": {"foo": "bar"},
+            }
+        )
+        request = self.make_mock_request(token_data=JSON_FILE_CONTENT)
+        subject_token = credentials.retrieve_subject_token(request)
+
+        assert subject_token == JSON_FILE_SUBJECT_TOKEN
+        self.assert_credential_request_kwargs(
+            request.call_args_list[0][1], {"foo": "bar"}
+        )
+
+    def test_retrieve_subject_token_from_url_not_found(self):
+        credentials = self.make_credentials(
+            credential_source=self.CREDENTIAL_SOURCE_TEXT_URL
+        )
+        with pytest.raises(exceptions.RefreshError) as excinfo:
+            credentials.retrieve_subject_token(
+                self.make_mock_request(token_status=404, token_data=JSON_FILE_CONTENT)
+            )
+
+        assert excinfo.match("Unable to retrieve Identity Pool subject token")
+
+    def test_retrieve_subject_token_from_url_json_invalid_field(self):
+        credential_source = {
+            "url": self.CREDENTIAL_URL,
+            "format": {"type": "json", "subject_token_field_name": "not_found"},
+        }
+        credentials = self.make_credentials(credential_source=credential_source)
+
+        with pytest.raises(exceptions.RefreshError) as excinfo:
+            credentials.retrieve_subject_token(
+                self.make_mock_request(token_data=JSON_FILE_CONTENT)
+            )
+
+        assert excinfo.match(
+            "Unable to parse subject_token from JSON file '{}' using key '{}'".format(
+                self.CREDENTIAL_URL, "not_found"
+            )
+        )
+
+    def test_retrieve_subject_token_from_url_json_invalid_format(self):
+        credentials = self.make_credentials(
+            credential_source=self.CREDENTIAL_SOURCE_JSON_URL
+        )
+
+        with pytest.raises(exceptions.RefreshError) as excinfo:
+            credentials.retrieve_subject_token(self.make_mock_request(token_data="{"))
+
+        assert excinfo.match(
+            "Unable to parse subject_token from JSON file '{}' using key '{}'".format(
+                self.CREDENTIAL_URL, "access_token"
+            )
+        )
+
+    def test_refresh_text_file_success_without_impersonation_url(self):
+        credentials = self.make_credentials(
+            client_id=CLIENT_ID,
+            client_secret=CLIENT_SECRET,
+            # Test with text format type.
+            credential_source=self.CREDENTIAL_SOURCE_TEXT_URL,
+            scopes=SCOPES,
+        )
+
+        self.assert_underlying_credentials_refresh(
+            credentials=credentials,
+            audience=AUDIENCE,
+            subject_token=TEXT_FILE_SUBJECT_TOKEN,
+            subject_token_type=SUBJECT_TOKEN_TYPE,
+            token_url=TOKEN_URL,
+            service_account_impersonation_url=None,
+            basic_auth_encoding=BASIC_AUTH_ENCODING,
+            quota_project_id=None,
+            used_scopes=SCOPES,
+            scopes=SCOPES,
+            default_scopes=None,
+            credential_data=TEXT_FILE_SUBJECT_TOKEN,
+        )
+
+    def test_refresh_text_file_success_with_impersonation_url(self):
+        # Initialize credentials with service account impersonation and basic auth.
+        credentials = self.make_credentials(
+            # Test with text format type.
+            credential_source=self.CREDENTIAL_SOURCE_TEXT_URL,
+            service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+            scopes=SCOPES,
+        )
+
+        self.assert_underlying_credentials_refresh(
+            credentials=credentials,
+            audience=AUDIENCE,
+            subject_token=TEXT_FILE_SUBJECT_TOKEN,
+            subject_token_type=SUBJECT_TOKEN_TYPE,
+            token_url=TOKEN_URL,
+            service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+            basic_auth_encoding=None,
+            quota_project_id=None,
+            used_scopes=SCOPES,
+            scopes=SCOPES,
+            default_scopes=None,
+            credential_data=TEXT_FILE_SUBJECT_TOKEN,
+        )
+
+    def test_refresh_json_file_success_without_impersonation_url(self):
+        credentials = self.make_credentials(
+            client_id=CLIENT_ID,
+            client_secret=CLIENT_SECRET,
+            # Test with JSON format type.
+            credential_source=self.CREDENTIAL_SOURCE_JSON_URL,
+            scopes=SCOPES,
+        )
+
+        self.assert_underlying_credentials_refresh(
+            credentials=credentials,
+            audience=AUDIENCE,
+            subject_token=JSON_FILE_SUBJECT_TOKEN,
+            subject_token_type=SUBJECT_TOKEN_TYPE,
+            token_url=TOKEN_URL,
+            service_account_impersonation_url=None,
+            basic_auth_encoding=BASIC_AUTH_ENCODING,
+            quota_project_id=None,
+            used_scopes=SCOPES,
+            scopes=SCOPES,
+            default_scopes=None,
+            credential_data=JSON_FILE_CONTENT,
+        )
+
+    def test_refresh_json_file_success_with_impersonation_url(self):
+        # Initialize credentials with service account impersonation and basic auth.
+        credentials = self.make_credentials(
+            # Test with JSON format type.
+            credential_source=self.CREDENTIAL_SOURCE_JSON_URL,
+            service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+            scopes=SCOPES,
+        )
+
+        self.assert_underlying_credentials_refresh(
+            credentials=credentials,
+            audience=AUDIENCE,
+            subject_token=JSON_FILE_SUBJECT_TOKEN,
+            subject_token_type=SUBJECT_TOKEN_TYPE,
+            token_url=TOKEN_URL,
+            service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL,
+            basic_auth_encoding=None,
+            quota_project_id=None,
+            used_scopes=SCOPES,
+            scopes=SCOPES,
+            default_scopes=None,
+            credential_data=JSON_FILE_CONTENT,
+        )
+
+    def test_refresh_with_retrieve_subject_token_error_url(self):
+        credential_source = {
+            "url": self.CREDENTIAL_URL,
+            "format": {"type": "json", "subject_token_field_name": "not_found"},
+        }
+        credentials = self.make_credentials(credential_source=credential_source)
+
+        with pytest.raises(exceptions.RefreshError) as excinfo:
+            credentials.refresh(self.make_mock_request(token_data=JSON_FILE_CONTENT))
+
+        assert excinfo.match(
+            "Unable to parse subject_token from JSON file '{}' using key '{}'".format(
+                self.CREDENTIAL_URL, "not_found"
+            )
+        )
diff --git a/tests/test_impersonated_credentials.py b/tests/test_impersonated_credentials.py
new file mode 100644
index 0000000..bc404e3
--- /dev/null
+++ b/tests/test_impersonated_credentials.py
@@ -0,0 +1,553 @@
+# Copyright 2018 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import datetime
+import json
+import os
+
+import mock
+import pytest
+from six.moves import http_client
+
+from google.auth import _helpers
+from google.auth import crypt
+from google.auth import exceptions
+from google.auth import impersonated_credentials
+from google.auth import transport
+from google.auth.impersonated_credentials import Credentials
+from google.oauth2 import credentials
+from google.oauth2 import service_account
+
+DATA_DIR = os.path.join(os.path.dirname(__file__), "", "data")
+
+with open(os.path.join(DATA_DIR, "privatekey.pem"), "rb") as fh:
+    PRIVATE_KEY_BYTES = fh.read()
+
+SERVICE_ACCOUNT_JSON_FILE = os.path.join(DATA_DIR, "service_account.json")
+
+ID_TOKEN_DATA = (
+    "eyJhbGciOiJSUzI1NiIsImtpZCI6ImRmMzc1ODkwOGI3OTIyOTNhZDk3N2Ew"
+    "Yjk5MWQ5OGE3N2Y0ZWVlY2QiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwc"
+    "zovL2Zvby5iYXIiLCJhenAiOiIxMDIxMDE1NTA4MzQyMDA3MDg1NjgiLCJle"
+    "HAiOjE1NjQ0NzUwNTEsImlhdCI6MTU2NDQ3MTQ1MSwiaXNzIjoiaHR0cHM6L"
+    "y9hY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTAyMTAxNTUwODM0MjAwN"
+    "zA4NTY4In0.redacted"
+)
+ID_TOKEN_EXPIRY = 1564475051
+
+with open(SERVICE_ACCOUNT_JSON_FILE, "r") as fh:
+    SERVICE_ACCOUNT_INFO = json.load(fh)
+
+SIGNER = crypt.RSASigner.from_string(PRIVATE_KEY_BYTES, "1")
+TOKEN_URI = "https://example.com/oauth2/token"
+
+
[email protected]
+def mock_donor_credentials():
+    with mock.patch("google.oauth2._client.jwt_grant", autospec=True) as grant:
+        grant.return_value = (
+            "source token",
+            _helpers.utcnow() + datetime.timedelta(seconds=500),
+            {},
+        )
+        yield grant
+
+
+class MockResponse:
+    def __init__(self, json_data, status_code):
+        self.json_data = json_data
+        self.status_code = status_code
+
+    def json(self):
+        return self.json_data
+
+
[email protected]
+def mock_authorizedsession_sign():
+    with mock.patch(
+        "google.auth.transport.requests.AuthorizedSession.request", autospec=True
+    ) as auth_session:
+        data = {"keyId": "1", "signedBlob": "c2lnbmF0dXJl"}
+        auth_session.return_value = MockResponse(data, http_client.OK)
+        yield auth_session
+
+
[email protected]
+def mock_authorizedsession_idtoken():
+    with mock.patch(
+        "google.auth.transport.requests.AuthorizedSession.request", autospec=True
+    ) as auth_session:
+        data = {"token": ID_TOKEN_DATA}
+        auth_session.return_value = MockResponse(data, http_client.OK)
+        yield auth_session
+
+
+class TestImpersonatedCredentials(object):
+
+    SERVICE_ACCOUNT_EMAIL = "[email protected]"
+    TARGET_PRINCIPAL = "[email protected]"
+    TARGET_SCOPES = ["https://www.googleapis.com/auth/devstorage.read_only"]
+    DELEGATES = []
+    LIFETIME = 3600
+    SOURCE_CREDENTIALS = service_account.Credentials(
+        SIGNER, SERVICE_ACCOUNT_EMAIL, TOKEN_URI
+    )
+    USER_SOURCE_CREDENTIALS = credentials.Credentials(token="ABCDE")
+    IAM_ENDPOINT_OVERRIDE = (
+        "https://us-east1-iamcredentials.googleapis.com/v1/projects/-"
+        + "/serviceAccounts/{}:generateAccessToken".format(SERVICE_ACCOUNT_EMAIL)
+    )
+
+    def make_credentials(
+        self,
+        source_credentials=SOURCE_CREDENTIALS,
+        lifetime=LIFETIME,
+        target_principal=TARGET_PRINCIPAL,
+        iam_endpoint_override=None,
+    ):
+
+        return Credentials(
+            source_credentials=source_credentials,
+            target_principal=target_principal,
+            target_scopes=self.TARGET_SCOPES,
+            delegates=self.DELEGATES,
+            lifetime=lifetime,
+            iam_endpoint_override=iam_endpoint_override,
+        )
+
+    def test_make_from_user_credentials(self):
+        credentials = self.make_credentials(
+            source_credentials=self.USER_SOURCE_CREDENTIALS
+        )
+        assert not credentials.valid
+        assert credentials.expired
+
+    def test_default_state(self):
+        credentials = self.make_credentials()
+        assert not credentials.valid
+        assert credentials.expired
+
+    def make_request(
+        self,
+        data,
+        status=http_client.OK,
+        headers=None,
+        side_effect=None,
+        use_data_bytes=True,
+    ):
+        response = mock.create_autospec(transport.Response, instance=False)
+        response.status = status
+        response.data = _helpers.to_bytes(data) if use_data_bytes else data
+        response.headers = headers or {}
+
+        request = mock.create_autospec(transport.Request, instance=False)
+        request.side_effect = side_effect
+        request.return_value = response
+
+        return request
+
+    @pytest.mark.parametrize("use_data_bytes", [True, False])
+    def test_refresh_success(self, use_data_bytes, mock_donor_credentials):
+        credentials = self.make_credentials(lifetime=None)
+        token = "token"
+
+        expire_time = (
+            _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=500)
+        ).isoformat("T") + "Z"
+        response_body = {"accessToken": token, "expireTime": expire_time}
+
+        request = self.make_request(
+            data=json.dumps(response_body),
+            status=http_client.OK,
+            use_data_bytes=use_data_bytes,
+        )
+
+        credentials.refresh(request)
+
+        assert credentials.valid
+        assert not credentials.expired
+
+    @pytest.mark.parametrize("use_data_bytes", [True, False])
+    def test_refresh_success_iam_endpoint_override(
+        self, use_data_bytes, mock_donor_credentials
+    ):
+        credentials = self.make_credentials(
+            lifetime=None, iam_endpoint_override=self.IAM_ENDPOINT_OVERRIDE
+        )
+        token = "token"
+
+        expire_time = (
+            _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=500)
+        ).isoformat("T") + "Z"
+        response_body = {"accessToken": token, "expireTime": expire_time}
+
+        request = self.make_request(
+            data=json.dumps(response_body),
+            status=http_client.OK,
+            use_data_bytes=use_data_bytes,
+        )
+
+        credentials.refresh(request)
+
+        assert credentials.valid
+        assert not credentials.expired
+        # Confirm override endpoint used.
+        request_kwargs = request.call_args[1]
+        assert request_kwargs["url"] == self.IAM_ENDPOINT_OVERRIDE
+
+    @pytest.mark.parametrize("time_skew", [100, -100])
+    def test_refresh_source_credentials(self, time_skew):
+        credentials = self.make_credentials(lifetime=None)
+
+        # Source credentials is refreshed only if it is expired within
+        # _helpers.REFRESH_THRESHOLD from now. We add a time_skew to the expiry, so
+        # source credentials is refreshed only if time_skew <= 0.
+        credentials._source_credentials.expiry = (
+            _helpers.utcnow()
+            + _helpers.REFRESH_THRESHOLD
+            + datetime.timedelta(seconds=time_skew)
+        )
+        credentials._source_credentials.token = "Token"
+
+        with mock.patch(
+            "google.oauth2.service_account.Credentials.refresh", autospec=True
+        ) as source_cred_refresh:
+            expire_time = (
+                _helpers.utcnow().replace(microsecond=0)
+                + datetime.timedelta(seconds=500)
+            ).isoformat("T") + "Z"
+            response_body = {"accessToken": "token", "expireTime": expire_time}
+            request = self.make_request(
+                data=json.dumps(response_body), status=http_client.OK
+            )
+
+            credentials.refresh(request)
+
+            assert credentials.valid
+            assert not credentials.expired
+
+            # Source credentials is refreshed only if it is expired within
+            # _helpers.REFRESH_THRESHOLD
+            if time_skew > 0:
+                source_cred_refresh.assert_not_called()
+            else:
+                source_cred_refresh.assert_called_once()
+
+    def test_refresh_failure_malformed_expire_time(self, mock_donor_credentials):
+        credentials = self.make_credentials(lifetime=None)
+        token = "token"
+
+        expire_time = (_helpers.utcnow() + datetime.timedelta(seconds=500)).isoformat(
+            "T"
+        )
+        response_body = {"accessToken": token, "expireTime": expire_time}
+
+        request = self.make_request(
+            data=json.dumps(response_body), status=http_client.OK
+        )
+
+        with pytest.raises(exceptions.RefreshError) as excinfo:
+            credentials.refresh(request)
+
+        assert excinfo.match(impersonated_credentials._REFRESH_ERROR)
+
+        assert not credentials.valid
+        assert credentials.expired
+
+    def test_refresh_failure_unauthorzed(self, mock_donor_credentials):
+        credentials = self.make_credentials(lifetime=None)
+
+        response_body = {
+            "error": {
+                "code": 403,
+                "message": "The caller does not have permission",
+                "status": "PERMISSION_DENIED",
+            }
+        }
+
+        request = self.make_request(
+            data=json.dumps(response_body), status=http_client.UNAUTHORIZED
+        )
+
+        with pytest.raises(exceptions.RefreshError) as excinfo:
+            credentials.refresh(request)
+
+        assert excinfo.match(impersonated_credentials._REFRESH_ERROR)
+
+        assert not credentials.valid
+        assert credentials.expired
+
+    def test_refresh_failure_http_error(self, mock_donor_credentials):
+        credentials = self.make_credentials(lifetime=None)
+
+        response_body = {}
+
+        request = self.make_request(
+            data=json.dumps(response_body), status=http_client.HTTPException
+        )
+
+        with pytest.raises(exceptions.RefreshError) as excinfo:
+            credentials.refresh(request)
+
+        assert excinfo.match(impersonated_credentials._REFRESH_ERROR)
+
+        assert not credentials.valid
+        assert credentials.expired
+
+    def test_expired(self):
+        credentials = self.make_credentials(lifetime=None)
+        assert credentials.expired
+
+    def test_signer(self):
+        credentials = self.make_credentials()
+        assert isinstance(credentials.signer, impersonated_credentials.Credentials)
+
+    def test_signer_email(self):
+        credentials = self.make_credentials(target_principal=self.TARGET_PRINCIPAL)
+        assert credentials.signer_email == self.TARGET_PRINCIPAL
+
+    def test_service_account_email(self):
+        credentials = self.make_credentials(target_principal=self.TARGET_PRINCIPAL)
+        assert credentials.service_account_email == self.TARGET_PRINCIPAL
+
+    def test_sign_bytes(self, mock_donor_credentials, mock_authorizedsession_sign):
+        credentials = self.make_credentials(lifetime=None)
+        token = "token"
+
+        expire_time = (
+            _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=500)
+        ).isoformat("T") + "Z"
+        token_response_body = {"accessToken": token, "expireTime": expire_time}
+
+        response = mock.create_autospec(transport.Response, instance=False)
+        response.status = http_client.OK
+        response.data = _helpers.to_bytes(json.dumps(token_response_body))
+
+        request = mock.create_autospec(transport.Request, instance=False)
+        request.return_value = response
+
+        credentials.refresh(request)
+
+        assert credentials.valid
+        assert not credentials.expired
+
+        signature = credentials.sign_bytes(b"signed bytes")
+        assert signature == b"signature"
+
+    def test_sign_bytes_failure(self):
+        credentials = self.make_credentials(lifetime=None)
+
+        with mock.patch(
+            "google.auth.transport.requests.AuthorizedSession.request", autospec=True
+        ) as auth_session:
+            data = {"error": {"code": 403, "message": "unauthorized"}}
+            auth_session.return_value = MockResponse(data, http_client.FORBIDDEN)
+
+            with pytest.raises(exceptions.TransportError) as excinfo:
+                credentials.sign_bytes(b"foo")
+            assert excinfo.match("'code': 403")
+
+    def test_with_quota_project(self):
+        credentials = self.make_credentials()
+
+        quota_project_creds = credentials.with_quota_project("project-foo")
+        assert quota_project_creds._quota_project_id == "project-foo"
+
+    @pytest.mark.parametrize("use_data_bytes", [True, False])
+    def test_with_quota_project_iam_endpoint_override(
+        self, use_data_bytes, mock_donor_credentials
+    ):
+        credentials = self.make_credentials(
+            lifetime=None, iam_endpoint_override=self.IAM_ENDPOINT_OVERRIDE
+        )
+        token = "token"
+        # iam_endpoint_override should be copied to created credentials.
+        quota_project_creds = credentials.with_quota_project("project-foo")
+
+        expire_time = (
+            _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=500)
+        ).isoformat("T") + "Z"
+        response_body = {"accessToken": token, "expireTime": expire_time}
+
+        request = self.make_request(
+            data=json.dumps(response_body),
+            status=http_client.OK,
+            use_data_bytes=use_data_bytes,
+        )
+
+        quota_project_creds.refresh(request)
+
+        assert quota_project_creds.valid
+        assert not quota_project_creds.expired
+        # Confirm override endpoint used.
+        request_kwargs = request.call_args[1]
+        assert request_kwargs["url"] == self.IAM_ENDPOINT_OVERRIDE
+
+    def test_id_token_success(
+        self, mock_donor_credentials, mock_authorizedsession_idtoken
+    ):
+        credentials = self.make_credentials(lifetime=None)
+        token = "token"
+        target_audience = "https://foo.bar"
+
+        expire_time = (
+            _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=500)
+        ).isoformat("T") + "Z"
+        response_body = {"accessToken": token, "expireTime": expire_time}
+
+        request = self.make_request(
+            data=json.dumps(response_body), status=http_client.OK
+        )
+
+        credentials.refresh(request)
+
+        assert credentials.valid
+        assert not credentials.expired
+
+        id_creds = impersonated_credentials.IDTokenCredentials(
+            credentials, target_audience=target_audience
+        )
+        id_creds.refresh(request)
+
+        assert id_creds.token == ID_TOKEN_DATA
+        assert id_creds.expiry == datetime.datetime.fromtimestamp(ID_TOKEN_EXPIRY)
+
+    def test_id_token_from_credential(
+        self, mock_donor_credentials, mock_authorizedsession_idtoken
+    ):
+        credentials = self.make_credentials(lifetime=None)
+        token = "token"
+        target_audience = "https://foo.bar"
+
+        expire_time = (
+            _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=500)
+        ).isoformat("T") + "Z"
+        response_body = {"accessToken": token, "expireTime": expire_time}
+
+        request = self.make_request(
+            data=json.dumps(response_body), status=http_client.OK
+        )
+
+        credentials.refresh(request)
+
+        assert credentials.valid
+        assert not credentials.expired
+
+        id_creds = impersonated_credentials.IDTokenCredentials(
+            credentials, target_audience=target_audience, include_email=True
+        )
+        id_creds = id_creds.from_credentials(target_credentials=credentials)
+        id_creds.refresh(request)
+
+        assert id_creds.token == ID_TOKEN_DATA
+        assert id_creds._include_email is True
+
+    def test_id_token_with_target_audience(
+        self, mock_donor_credentials, mock_authorizedsession_idtoken
+    ):
+        credentials = self.make_credentials(lifetime=None)
+        token = "token"
+        target_audience = "https://foo.bar"
+
+        expire_time = (
+            _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=500)
+        ).isoformat("T") + "Z"
+        response_body = {"accessToken": token, "expireTime": expire_time}
+
+        request = self.make_request(
+            data=json.dumps(response_body), status=http_client.OK
+        )
+
+        credentials.refresh(request)
+
+        assert credentials.valid
+        assert not credentials.expired
+
+        id_creds = impersonated_credentials.IDTokenCredentials(
+            credentials, include_email=True
+        )
+        id_creds = id_creds.with_target_audience(target_audience=target_audience)
+        id_creds.refresh(request)
+
+        assert id_creds.token == ID_TOKEN_DATA
+        assert id_creds.expiry == datetime.datetime.fromtimestamp(ID_TOKEN_EXPIRY)
+        assert id_creds._include_email is True
+
+    def test_id_token_invalid_cred(
+        self, mock_donor_credentials, mock_authorizedsession_idtoken
+    ):
+        credentials = None
+
+        with pytest.raises(exceptions.GoogleAuthError) as excinfo:
+            impersonated_credentials.IDTokenCredentials(credentials)
+
+        assert excinfo.match("Provided Credential must be" " impersonated_credentials")
+
+    def test_id_token_with_include_email(
+        self, mock_donor_credentials, mock_authorizedsession_idtoken
+    ):
+        credentials = self.make_credentials(lifetime=None)
+        token = "token"
+        target_audience = "https://foo.bar"
+
+        expire_time = (
+            _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=500)
+        ).isoformat("T") + "Z"
+        response_body = {"accessToken": token, "expireTime": expire_time}
+
+        request = self.make_request(
+            data=json.dumps(response_body), status=http_client.OK
+        )
+
+        credentials.refresh(request)
+
+        assert credentials.valid
+        assert not credentials.expired
+
+        id_creds = impersonated_credentials.IDTokenCredentials(
+            credentials, target_audience=target_audience
+        )
+        id_creds = id_creds.with_include_email(True)
+        id_creds.refresh(request)
+
+        assert id_creds.token == ID_TOKEN_DATA
+
+    def test_id_token_with_quota_project(
+        self, mock_donor_credentials, mock_authorizedsession_idtoken
+    ):
+        credentials = self.make_credentials(lifetime=None)
+        token = "token"
+        target_audience = "https://foo.bar"
+
+        expire_time = (
+            _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=500)
+        ).isoformat("T") + "Z"
+        response_body = {"accessToken": token, "expireTime": expire_time}
+
+        request = self.make_request(
+            data=json.dumps(response_body), status=http_client.OK
+        )
+
+        credentials.refresh(request)
+
+        assert credentials.valid
+        assert not credentials.expired
+
+        id_creds = impersonated_credentials.IDTokenCredentials(
+            credentials, target_audience=target_audience
+        )
+        id_creds = id_creds.with_quota_project("project-foo")
+        id_creds.refresh(request)
+
+        assert id_creds.quota_project_id == "project-foo"
diff --git a/tests/test_jwt.py b/tests/test_jwt.py
new file mode 100644
index 0000000..c0e1184
--- /dev/null
+++ b/tests/test_jwt.py
@@ -0,0 +1,646 @@
+# Copyright 2014 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import base64
+import datetime
+import json
+import os
+
+import mock
+import pytest
+
+from google.auth import _helpers
+from google.auth import crypt
+from google.auth import exceptions
+from google.auth import jwt
+
+
+DATA_DIR = os.path.join(os.path.dirname(__file__), "data")
+
+with open(os.path.join(DATA_DIR, "privatekey.pem"), "rb") as fh:
+    PRIVATE_KEY_BYTES = fh.read()
+
+with open(os.path.join(DATA_DIR, "public_cert.pem"), "rb") as fh:
+    PUBLIC_CERT_BYTES = fh.read()
+
+with open(os.path.join(DATA_DIR, "other_cert.pem"), "rb") as fh:
+    OTHER_CERT_BYTES = fh.read()
+
+with open(os.path.join(DATA_DIR, "es256_privatekey.pem"), "rb") as fh:
+    EC_PRIVATE_KEY_BYTES = fh.read()
+
+with open(os.path.join(DATA_DIR, "es256_public_cert.pem"), "rb") as fh:
+    EC_PUBLIC_CERT_BYTES = fh.read()
+
+SERVICE_ACCOUNT_JSON_FILE = os.path.join(DATA_DIR, "service_account.json")
+
+with open(SERVICE_ACCOUNT_JSON_FILE, "r") as fh:
+    SERVICE_ACCOUNT_INFO = json.load(fh)
+
+
[email protected]
+def signer():
+    return crypt.RSASigner.from_string(PRIVATE_KEY_BYTES, "1")
+
+
+def test_encode_basic(signer):
+    test_payload = {"test": "value"}
+    encoded = jwt.encode(signer, test_payload)
+    header, payload, _, _ = jwt._unverified_decode(encoded)
+    assert payload == test_payload
+    assert header == {"typ": "JWT", "alg": "RS256", "kid": signer.key_id}
+
+
+def test_encode_extra_headers(signer):
+    encoded = jwt.encode(signer, {}, header={"extra": "value"})
+    header = jwt.decode_header(encoded)
+    assert header == {
+        "typ": "JWT",
+        "alg": "RS256",
+        "kid": signer.key_id,
+        "extra": "value",
+    }
+
+
+def test_encode_custom_alg_in_headers(signer):
+    encoded = jwt.encode(signer, {}, header={"alg": "foo"})
+    header = jwt.decode_header(encoded)
+    assert header == {"typ": "JWT", "alg": "foo", "kid": signer.key_id}
+
+
[email protected]
+def es256_signer():
+    return crypt.ES256Signer.from_string(EC_PRIVATE_KEY_BYTES, "1")
+
+
+def test_encode_basic_es256(es256_signer):
+    test_payload = {"test": "value"}
+    encoded = jwt.encode(es256_signer, test_payload)
+    header, payload, _, _ = jwt._unverified_decode(encoded)
+    assert payload == test_payload
+    assert header == {"typ": "JWT", "alg": "ES256", "kid": es256_signer.key_id}
+
+
[email protected]
+def token_factory(signer, es256_signer):
+    def factory(claims=None, key_id=None, use_es256_signer=False):
+        now = _helpers.datetime_to_secs(_helpers.utcnow())
+        payload = {
+            "aud": "[email protected]",
+            "iat": now,
+            "exp": now + 300,
+            "user": "billy bob",
+            "metadata": {"meta": "data"},
+        }
+        payload.update(claims or {})
+
+        # False is specified to remove the signer's key id for testing
+        # headers without key ids.
+        if key_id is False:
+            signer._key_id = None
+            key_id = None
+
+        if use_es256_signer:
+            return jwt.encode(es256_signer, payload, key_id=key_id)
+        else:
+            return jwt.encode(signer, payload, key_id=key_id)
+
+    return factory
+
+
+def test_decode_valid(token_factory):
+    payload = jwt.decode(token_factory(), certs=PUBLIC_CERT_BYTES)
+    assert payload["aud"] == "[email protected]"
+    assert payload["user"] == "billy bob"
+    assert payload["metadata"]["meta"] == "data"
+
+
+def test_decode_valid_es256(token_factory):
+    payload = jwt.decode(
+        token_factory(use_es256_signer=True), certs=EC_PUBLIC_CERT_BYTES
+    )
+    assert payload["aud"] == "[email protected]"
+    assert payload["user"] == "billy bob"
+    assert payload["metadata"]["meta"] == "data"
+
+
+def test_decode_valid_with_audience(token_factory):
+    payload = jwt.decode(
+        token_factory(), certs=PUBLIC_CERT_BYTES, audience="[email protected]"
+    )
+    assert payload["aud"] == "[email protected]"
+    assert payload["user"] == "billy bob"
+    assert payload["metadata"]["meta"] == "data"
+
+
+def test_decode_valid_with_audience_list(token_factory):
+    payload = jwt.decode(
+        token_factory(),
+        certs=PUBLIC_CERT_BYTES,
+        audience=["[email protected]", "[email protected]"],
+    )
+    assert payload["aud"] == "[email protected]"
+    assert payload["user"] == "billy bob"
+    assert payload["metadata"]["meta"] == "data"
+
+
+def test_decode_valid_unverified(token_factory):
+    payload = jwt.decode(token_factory(), certs=OTHER_CERT_BYTES, verify=False)
+    assert payload["aud"] == "[email protected]"
+    assert payload["user"] == "billy bob"
+    assert payload["metadata"]["meta"] == "data"
+
+
+def test_decode_bad_token_wrong_number_of_segments():
+    with pytest.raises(ValueError) as excinfo:
+        jwt.decode("1.2", PUBLIC_CERT_BYTES)
+    assert excinfo.match(r"Wrong number of segments")
+
+
+def test_decode_bad_token_not_base64():
+    with pytest.raises((ValueError, TypeError)) as excinfo:
+        jwt.decode("1.2.3", PUBLIC_CERT_BYTES)
+    assert excinfo.match(r"Incorrect padding|more than a multiple of 4")
+
+
+def test_decode_bad_token_not_json():
+    token = b".".join([base64.urlsafe_b64encode(b"123!")] * 3)
+    with pytest.raises(ValueError) as excinfo:
+        jwt.decode(token, PUBLIC_CERT_BYTES)
+    assert excinfo.match(r"Can\'t parse segment")
+
+
+def test_decode_bad_token_no_iat_or_exp(signer):
+    token = jwt.encode(signer, {"test": "value"})
+    with pytest.raises(ValueError) as excinfo:
+        jwt.decode(token, PUBLIC_CERT_BYTES)
+    assert excinfo.match(r"Token does not contain required claim")
+
+
+def test_decode_bad_token_too_early(token_factory):
+    token = token_factory(
+        claims={
+            "iat": _helpers.datetime_to_secs(
+                _helpers.utcnow() + datetime.timedelta(hours=1)
+            )
+        }
+    )
+    with pytest.raises(ValueError) as excinfo:
+        jwt.decode(token, PUBLIC_CERT_BYTES, clock_skew_in_seconds=59)
+    assert excinfo.match(r"Token used too early")
+
+
+def test_decode_bad_token_expired(token_factory):
+    token = token_factory(
+        claims={
+            "exp": _helpers.datetime_to_secs(
+                _helpers.utcnow() - datetime.timedelta(hours=1)
+            )
+        }
+    )
+    with pytest.raises(ValueError) as excinfo:
+        jwt.decode(token, PUBLIC_CERT_BYTES, clock_skew_in_seconds=59)
+    assert excinfo.match(r"Token expired")
+
+
+def test_decode_success_with_no_clock_skew(token_factory):
+    token = token_factory(
+        claims={
+            "exp": _helpers.datetime_to_secs(
+                _helpers.utcnow() + datetime.timedelta(seconds=1)
+            ),
+            "iat": _helpers.datetime_to_secs(
+                _helpers.utcnow() - datetime.timedelta(seconds=1)
+            ),
+        }
+    )
+
+    jwt.decode(token, PUBLIC_CERT_BYTES)
+
+
+def test_decode_success_with_custom_clock_skew(token_factory):
+    token = token_factory(
+        claims={
+            "exp": _helpers.datetime_to_secs(
+                _helpers.utcnow() + datetime.timedelta(seconds=2)
+            ),
+            "iat": _helpers.datetime_to_secs(
+                _helpers.utcnow() - datetime.timedelta(seconds=2)
+            ),
+        }
+    )
+
+    jwt.decode(token, PUBLIC_CERT_BYTES, clock_skew_in_seconds=1)
+
+
+def test_decode_bad_token_wrong_audience(token_factory):
+    token = token_factory()
+    audience = "[email protected]"
+    with pytest.raises(ValueError) as excinfo:
+        jwt.decode(token, PUBLIC_CERT_BYTES, audience=audience)
+    assert excinfo.match(r"Token has wrong audience")
+
+
+def test_decode_bad_token_wrong_audience_list(token_factory):
+    token = token_factory()
+    audience = ["[email protected]", "[email protected]"]
+    with pytest.raises(ValueError) as excinfo:
+        jwt.decode(token, PUBLIC_CERT_BYTES, audience=audience)
+    assert excinfo.match(r"Token has wrong audience")
+
+
+def test_decode_wrong_cert(token_factory):
+    with pytest.raises(ValueError) as excinfo:
+        jwt.decode(token_factory(), OTHER_CERT_BYTES)
+    assert excinfo.match(r"Could not verify token signature")
+
+
+def test_decode_multicert_bad_cert(token_factory):
+    certs = {"1": OTHER_CERT_BYTES, "2": PUBLIC_CERT_BYTES}
+    with pytest.raises(ValueError) as excinfo:
+        jwt.decode(token_factory(), certs)
+    assert excinfo.match(r"Could not verify token signature")
+
+
+def test_decode_no_cert(token_factory):
+    certs = {"2": PUBLIC_CERT_BYTES}
+    with pytest.raises(ValueError) as excinfo:
+        jwt.decode(token_factory(), certs)
+    assert excinfo.match(r"Certificate for key id 1 not found")
+
+
+def test_decode_no_key_id(token_factory):
+    token = token_factory(key_id=False)
+    certs = {"2": PUBLIC_CERT_BYTES}
+    payload = jwt.decode(token, certs)
+    assert payload["user"] == "billy bob"
+
+
+def test_decode_unknown_alg():
+    headers = json.dumps({u"kid": u"1", u"alg": u"fakealg"})
+    token = b".".join(
+        map(lambda seg: base64.b64encode(seg.encode("utf-8")), [headers, u"{}", u"sig"])
+    )
+
+    with pytest.raises(ValueError) as excinfo:
+        jwt.decode(token)
+    assert excinfo.match(r"fakealg")
+
+
+def test_decode_missing_crytography_alg(monkeypatch):
+    monkeypatch.delitem(jwt._ALGORITHM_TO_VERIFIER_CLASS, "ES256")
+    headers = json.dumps({u"kid": u"1", u"alg": u"ES256"})
+    token = b".".join(
+        map(lambda seg: base64.b64encode(seg.encode("utf-8")), [headers, u"{}", u"sig"])
+    )
+
+    with pytest.raises(ValueError) as excinfo:
+        jwt.decode(token)
+    assert excinfo.match(r"cryptography")
+
+
+def test_roundtrip_explicit_key_id(token_factory):
+    token = token_factory(key_id="3")
+    certs = {"2": OTHER_CERT_BYTES, "3": PUBLIC_CERT_BYTES}
+    payload = jwt.decode(token, certs)
+    assert payload["user"] == "billy bob"
+
+
+class TestCredentials(object):
+    SERVICE_ACCOUNT_EMAIL = "[email protected]"
+    SUBJECT = "subject"
+    AUDIENCE = "audience"
+    ADDITIONAL_CLAIMS = {"meta": "data"}
+    credentials = None
+
+    @pytest.fixture(autouse=True)
+    def credentials_fixture(self, signer):
+        self.credentials = jwt.Credentials(
+            signer,
+            self.SERVICE_ACCOUNT_EMAIL,
+            self.SERVICE_ACCOUNT_EMAIL,
+            self.AUDIENCE,
+        )
+
+    def test_from_service_account_info(self):
+        with open(SERVICE_ACCOUNT_JSON_FILE, "r") as fh:
+            info = json.load(fh)
+
+        credentials = jwt.Credentials.from_service_account_info(
+            info, audience=self.AUDIENCE
+        )
+
+        assert credentials._signer.key_id == info["private_key_id"]
+        assert credentials._issuer == info["client_email"]
+        assert credentials._subject == info["client_email"]
+        assert credentials._audience == self.AUDIENCE
+
+    def test_from_service_account_info_args(self):
+        info = SERVICE_ACCOUNT_INFO.copy()
+
+        credentials = jwt.Credentials.from_service_account_info(
+            info,
+            subject=self.SUBJECT,
+            audience=self.AUDIENCE,
+            additional_claims=self.ADDITIONAL_CLAIMS,
+        )
+
+        assert credentials._signer.key_id == info["private_key_id"]
+        assert credentials._issuer == info["client_email"]
+        assert credentials._subject == self.SUBJECT
+        assert credentials._audience == self.AUDIENCE
+        assert credentials._additional_claims == self.ADDITIONAL_CLAIMS
+
+    def test_from_service_account_file(self):
+        info = SERVICE_ACCOUNT_INFO.copy()
+
+        credentials = jwt.Credentials.from_service_account_file(
+            SERVICE_ACCOUNT_JSON_FILE, audience=self.AUDIENCE
+        )
+
+        assert credentials._signer.key_id == info["private_key_id"]
+        assert credentials._issuer == info["client_email"]
+        assert credentials._subject == info["client_email"]
+        assert credentials._audience == self.AUDIENCE
+
+    def test_from_service_account_file_args(self):
+        info = SERVICE_ACCOUNT_INFO.copy()
+
+        credentials = jwt.Credentials.from_service_account_file(
+            SERVICE_ACCOUNT_JSON_FILE,
+            subject=self.SUBJECT,
+            audience=self.AUDIENCE,
+            additional_claims=self.ADDITIONAL_CLAIMS,
+        )
+
+        assert credentials._signer.key_id == info["private_key_id"]
+        assert credentials._issuer == info["client_email"]
+        assert credentials._subject == self.SUBJECT
+        assert credentials._audience == self.AUDIENCE
+        assert credentials._additional_claims == self.ADDITIONAL_CLAIMS
+
+    def test_from_signing_credentials(self):
+        jwt_from_signing = self.credentials.from_signing_credentials(
+            self.credentials, audience=mock.sentinel.new_audience
+        )
+        jwt_from_info = jwt.Credentials.from_service_account_info(
+            SERVICE_ACCOUNT_INFO, audience=mock.sentinel.new_audience
+        )
+
+        assert isinstance(jwt_from_signing, jwt.Credentials)
+        assert jwt_from_signing._signer.key_id == jwt_from_info._signer.key_id
+        assert jwt_from_signing._issuer == jwt_from_info._issuer
+        assert jwt_from_signing._subject == jwt_from_info._subject
+        assert jwt_from_signing._audience == jwt_from_info._audience
+
+    def test_default_state(self):
+        assert not self.credentials.valid
+        # Expiration hasn't been set yet
+        assert not self.credentials.expired
+
+    def test_with_claims(self):
+        new_audience = "new_audience"
+        new_credentials = self.credentials.with_claims(audience=new_audience)
+
+        assert new_credentials._signer == self.credentials._signer
+        assert new_credentials._issuer == self.credentials._issuer
+        assert new_credentials._subject == self.credentials._subject
+        assert new_credentials._audience == new_audience
+        assert new_credentials._additional_claims == self.credentials._additional_claims
+        assert new_credentials._quota_project_id == self.credentials._quota_project_id
+
+    def test__make_jwt_without_audience(self):
+        cred = jwt.Credentials.from_service_account_info(
+            SERVICE_ACCOUNT_INFO.copy(),
+            subject=self.SUBJECT,
+            audience=None,
+            additional_claims={"scope": "foo bar"},
+        )
+        token, _ = cred._make_jwt()
+        payload = jwt.decode(token, PUBLIC_CERT_BYTES)
+        assert payload["scope"] == "foo bar"
+        assert "aud" not in payload
+
+    def test_with_quota_project(self):
+        quota_project_id = "project-foo"
+
+        new_credentials = self.credentials.with_quota_project(quota_project_id)
+        assert new_credentials._signer == self.credentials._signer
+        assert new_credentials._issuer == self.credentials._issuer
+        assert new_credentials._subject == self.credentials._subject
+        assert new_credentials._audience == self.credentials._audience
+        assert new_credentials._additional_claims == self.credentials._additional_claims
+        assert new_credentials._quota_project_id == quota_project_id
+
+    def test_sign_bytes(self):
+        to_sign = b"123"
+        signature = self.credentials.sign_bytes(to_sign)
+        assert crypt.verify_signature(to_sign, signature, PUBLIC_CERT_BYTES)
+
+    def test_signer(self):
+        assert isinstance(self.credentials.signer, crypt.RSASigner)
+
+    def test_signer_email(self):
+        assert self.credentials.signer_email == SERVICE_ACCOUNT_INFO["client_email"]
+
+    def _verify_token(self, token):
+        payload = jwt.decode(token, PUBLIC_CERT_BYTES)
+        assert payload["iss"] == self.SERVICE_ACCOUNT_EMAIL
+        return payload
+
+    def test_refresh(self):
+        self.credentials.refresh(None)
+        assert self.credentials.valid
+        assert not self.credentials.expired
+
+    def test_expired(self):
+        assert not self.credentials.expired
+
+        self.credentials.refresh(None)
+        assert not self.credentials.expired
+
+        with mock.patch("google.auth._helpers.utcnow") as now:
+            one_day = datetime.timedelta(days=1)
+            now.return_value = self.credentials.expiry + one_day
+            assert self.credentials.expired
+
+    def test_before_request(self):
+        headers = {}
+
+        self.credentials.refresh(None)
+        self.credentials.before_request(
+            None, "GET", "http://example.com?a=1#3", headers
+        )
+
+        header_value = headers["authorization"]
+        _, token = header_value.split(" ")
+
+        # Since the audience is set, it should use the existing token.
+        assert token.encode("utf-8") == self.credentials.token
+
+        payload = self._verify_token(token)
+        assert payload["aud"] == self.AUDIENCE
+
+    def test_before_request_refreshes(self):
+        assert not self.credentials.valid
+        self.credentials.before_request(None, "GET", "http://example.com?a=1#3", {})
+        assert self.credentials.valid
+
+
+class TestOnDemandCredentials(object):
+    SERVICE_ACCOUNT_EMAIL = "[email protected]"
+    SUBJECT = "subject"
+    ADDITIONAL_CLAIMS = {"meta": "data"}
+    credentials = None
+
+    @pytest.fixture(autouse=True)
+    def credentials_fixture(self, signer):
+        self.credentials = jwt.OnDemandCredentials(
+            signer,
+            self.SERVICE_ACCOUNT_EMAIL,
+            self.SERVICE_ACCOUNT_EMAIL,
+            max_cache_size=2,
+        )
+
+    def test_from_service_account_info(self):
+        with open(SERVICE_ACCOUNT_JSON_FILE, "r") as fh:
+            info = json.load(fh)
+
+        credentials = jwt.OnDemandCredentials.from_service_account_info(info)
+
+        assert credentials._signer.key_id == info["private_key_id"]
+        assert credentials._issuer == info["client_email"]
+        assert credentials._subject == info["client_email"]
+
+    def test_from_service_account_info_args(self):
+        info = SERVICE_ACCOUNT_INFO.copy()
+
+        credentials = jwt.OnDemandCredentials.from_service_account_info(
+            info, subject=self.SUBJECT, additional_claims=self.ADDITIONAL_CLAIMS
+        )
+
+        assert credentials._signer.key_id == info["private_key_id"]
+        assert credentials._issuer == info["client_email"]
+        assert credentials._subject == self.SUBJECT
+        assert credentials._additional_claims == self.ADDITIONAL_CLAIMS
+
+    def test_from_service_account_file(self):
+        info = SERVICE_ACCOUNT_INFO.copy()
+
+        credentials = jwt.OnDemandCredentials.from_service_account_file(
+            SERVICE_ACCOUNT_JSON_FILE
+        )
+
+        assert credentials._signer.key_id == info["private_key_id"]
+        assert credentials._issuer == info["client_email"]
+        assert credentials._subject == info["client_email"]
+
+    def test_from_service_account_file_args(self):
+        info = SERVICE_ACCOUNT_INFO.copy()
+
+        credentials = jwt.OnDemandCredentials.from_service_account_file(
+            SERVICE_ACCOUNT_JSON_FILE,
+            subject=self.SUBJECT,
+            additional_claims=self.ADDITIONAL_CLAIMS,
+        )
+
+        assert credentials._signer.key_id == info["private_key_id"]
+        assert credentials._issuer == info["client_email"]
+        assert credentials._subject == self.SUBJECT
+        assert credentials._additional_claims == self.ADDITIONAL_CLAIMS
+
+    def test_from_signing_credentials(self):
+        jwt_from_signing = self.credentials.from_signing_credentials(self.credentials)
+        jwt_from_info = jwt.OnDemandCredentials.from_service_account_info(
+            SERVICE_ACCOUNT_INFO
+        )
+
+        assert isinstance(jwt_from_signing, jwt.OnDemandCredentials)
+        assert jwt_from_signing._signer.key_id == jwt_from_info._signer.key_id
+        assert jwt_from_signing._issuer == jwt_from_info._issuer
+        assert jwt_from_signing._subject == jwt_from_info._subject
+
+    def test_default_state(self):
+        # Credentials are *always* valid.
+        assert self.credentials.valid
+        # Credentials *never* expire.
+        assert not self.credentials.expired
+
+    def test_with_claims(self):
+        new_claims = {"meep": "moop"}
+        new_credentials = self.credentials.with_claims(additional_claims=new_claims)
+
+        assert new_credentials._signer == self.credentials._signer
+        assert new_credentials._issuer == self.credentials._issuer
+        assert new_credentials._subject == self.credentials._subject
+        assert new_credentials._additional_claims == new_claims
+
+    def test_with_quota_project(self):
+        quota_project_id = "project-foo"
+        new_credentials = self.credentials.with_quota_project(quota_project_id)
+
+        assert new_credentials._signer == self.credentials._signer
+        assert new_credentials._issuer == self.credentials._issuer
+        assert new_credentials._subject == self.credentials._subject
+        assert new_credentials._additional_claims == self.credentials._additional_claims
+        assert new_credentials._quota_project_id == quota_project_id
+
+    def test_sign_bytes(self):
+        to_sign = b"123"
+        signature = self.credentials.sign_bytes(to_sign)
+        assert crypt.verify_signature(to_sign, signature, PUBLIC_CERT_BYTES)
+
+    def test_signer(self):
+        assert isinstance(self.credentials.signer, crypt.RSASigner)
+
+    def test_signer_email(self):
+        assert self.credentials.signer_email == SERVICE_ACCOUNT_INFO["client_email"]
+
+    def _verify_token(self, token):
+        payload = jwt.decode(token, PUBLIC_CERT_BYTES)
+        assert payload["iss"] == self.SERVICE_ACCOUNT_EMAIL
+        return payload
+
+    def test_refresh(self):
+        with pytest.raises(exceptions.RefreshError):
+            self.credentials.refresh(None)
+
+    def test_before_request(self):
+        headers = {}
+
+        self.credentials.before_request(
+            None, "GET", "http://example.com?a=1#3", headers
+        )
+
+        _, token = headers["authorization"].split(" ")
+        payload = self._verify_token(token)
+
+        assert payload["aud"] == "http://example.com"
+
+        # Making another request should re-use the same token.
+        self.credentials.before_request(None, "GET", "http://example.com?b=2", headers)
+
+        _, new_token = headers["authorization"].split(" ")
+
+        assert new_token == token
+
+    def test_expired_token(self):
+        self.credentials._cache["audience"] = (
+            mock.sentinel.token,
+            datetime.datetime.min,
+        )
+
+        token = self.credentials._get_jwt_for_audience("audience")
+
+        assert token != mock.sentinel.token
diff --git a/tests/transport/__init__.py b/tests/transport/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/transport/__init__.py
diff --git a/tests/transport/compliance.py b/tests/transport/compliance.py
new file mode 100644
index 0000000..e093d76
--- /dev/null
+++ b/tests/transport/compliance.py
@@ -0,0 +1,108 @@
+# Copyright 2016 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 time
+
+import flask
+import pytest
+from pytest_localserver.http import WSGIServer
+from six.moves import http_client
+
+from google.auth import exceptions
+
+# .invalid will never resolve, see https://tools.ietf.org/html/rfc2606
+NXDOMAIN = "test.invalid"
+
+
+class RequestResponseTests(object):
+    @pytest.fixture(scope="module")
+    def server(self):
+        """Provides a test HTTP server.
+
+        The test server is automatically created before
+        a test and destroyed at the end. The server is serving a test
+        application that can be used to verify requests.
+        """
+        app = flask.Flask(__name__)
+        app.debug = True
+
+        # pylint: disable=unused-variable
+        # (pylint thinks the flask routes are unusued.)
+        @app.route("/basic")
+        def index():
+            header_value = flask.request.headers.get("x-test-header", "value")
+            headers = {"X-Test-Header": header_value}
+            return "Basic Content", http_client.OK, headers
+
+        @app.route("/server_error")
+        def server_error():
+            return "Error", http_client.INTERNAL_SERVER_ERROR
+
+        @app.route("/wait")
+        def wait():
+            time.sleep(3)
+            return "Waited"
+
+        # pylint: enable=unused-variable
+
+        server = WSGIServer(application=app.wsgi_app)
+        server.start()
+        yield server
+        server.stop()
+
+    def test_request_basic(self, server):
+        request = self.make_request()
+        response = request(url=server.url + "/basic", method="GET")
+
+        assert response.status == http_client.OK
+        assert response.headers["x-test-header"] == "value"
+        assert response.data == b"Basic Content"
+
+    def test_request_with_timeout_success(self, server):
+        request = self.make_request()
+        response = request(url=server.url + "/basic", method="GET", timeout=2)
+
+        assert response.status == http_client.OK
+        assert response.headers["x-test-header"] == "value"
+        assert response.data == b"Basic Content"
+
+    def test_request_with_timeout_failure(self, server):
+        request = self.make_request()
+
+        with pytest.raises(exceptions.TransportError):
+            request(url=server.url + "/wait", method="GET", timeout=1)
+
+    def test_request_headers(self, server):
+        request = self.make_request()
+        response = request(
+            url=server.url + "/basic",
+            method="GET",
+            headers={"x-test-header": "hello world"},
+        )
+
+        assert response.status == http_client.OK
+        assert response.headers["x-test-header"] == "hello world"
+        assert response.data == b"Basic Content"
+
+    def test_request_error(self, server):
+        request = self.make_request()
+        response = request(url=server.url + "/server_error", method="GET")
+
+        assert response.status == http_client.INTERNAL_SERVER_ERROR
+        assert response.data == b"Error"
+
+    def test_connection_error(self):
+        request = self.make_request()
+        with pytest.raises(exceptions.TransportError):
+            request(url="http://{}".format(NXDOMAIN), method="GET")
diff --git a/tests/transport/test__http_client.py b/tests/transport/test__http_client.py
new file mode 100644
index 0000000..c176cb2
--- /dev/null
+++ b/tests/transport/test__http_client.py
@@ -0,0 +1,31 @@
+# Copyright 2016 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 pytest
+
+from google.auth import exceptions
+import google.auth.transport._http_client
+from tests.transport import compliance
+
+
+class TestRequestResponse(compliance.RequestResponseTests):
+    def make_request(self):
+        return google.auth.transport._http_client.Request()
+
+    def test_non_http(self):
+        request = self.make_request()
+        with pytest.raises(exceptions.TransportError) as excinfo:
+            request(url="https://{}".format(compliance.NXDOMAIN), method="GET")
+
+        assert excinfo.match("https")
diff --git a/tests/transport/test__mtls_helper.py b/tests/transport/test__mtls_helper.py
new file mode 100644
index 0000000..3b6349a
--- /dev/null
+++ b/tests/transport/test__mtls_helper.py
@@ -0,0 +1,440 @@
+# 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.
+
+import os
+import re
+
+import mock
+from OpenSSL import crypto
+import pytest
+
+from google.auth import exceptions
+from google.auth.transport import _mtls_helper
+
+CONTEXT_AWARE_METADATA = {"cert_provider_command": ["some command"]}
+
+CONTEXT_AWARE_METADATA_NO_CERT_PROVIDER_COMMAND = {}
+
+ENCRYPTED_EC_PRIVATE_KEY = b"""-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIHkME8GCSqGSIb3DQEFDTBCMCkGCSqGSIb3DQEFDDAcBAgl2/yVgs1h3QICCAAw
+DAYIKoZIhvcNAgkFADAVBgkrBgEEAZdVAQIECJk2GRrvxOaJBIGQXIBnMU4wmciT
+uA6yD8q0FxuIzjG7E2S6tc5VRgSbhRB00eBO3jWmO2pBybeQW+zVioDcn50zp2ts
+wYErWC+LCm1Zg3r+EGnT1E1GgNoODbVQ3AEHlKh1CGCYhEovxtn3G+Fjh7xOBrNB
+saVVeDb4tHD4tMkiVVUBrUcTZPndP73CtgyGHYEphasYPzEz3+AU
+-----END ENCRYPTED PRIVATE KEY-----"""
+
+EC_PUBLIC_KEY = b"""-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEvCNi1NoDY1oMqPHIgXI8RBbTYGi/
+brEjbre1nSiQW11xRTJbVeETdsuP0EAu2tG3PcRhhwDfeJ8zXREgTBurNw==
+-----END PUBLIC KEY-----"""
+
+PASSPHRASE = b"""-----BEGIN PASSPHRASE-----
+password
+-----END PASSPHRASE-----"""
+PASSPHRASE_VALUE = b"password"
+
+
+def check_cert_and_key(content, expected_cert, expected_key):
+    success = True
+
+    cert_match = re.findall(_mtls_helper._CERT_REGEX, content)
+    success = success and len(cert_match) == 1 and cert_match[0] == expected_cert
+
+    key_match = re.findall(_mtls_helper._KEY_REGEX, content)
+    success = success and len(key_match) == 1 and key_match[0] == expected_key
+
+    return success
+
+
+class TestCertAndKeyRegex(object):
+    def test_cert_and_key(self):
+        # Test single cert and single key
+        check_cert_and_key(
+            pytest.public_cert_bytes + pytest.private_key_bytes,
+            pytest.public_cert_bytes,
+            pytest.private_key_bytes,
+        )
+        check_cert_and_key(
+            pytest.private_key_bytes + pytest.public_cert_bytes,
+            pytest.public_cert_bytes,
+            pytest.private_key_bytes,
+        )
+
+        # Test cert chain and single key
+        check_cert_and_key(
+            pytest.public_cert_bytes
+            + pytest.public_cert_bytes
+            + pytest.private_key_bytes,
+            pytest.public_cert_bytes + pytest.public_cert_bytes,
+            pytest.private_key_bytes,
+        )
+        check_cert_and_key(
+            pytest.private_key_bytes
+            + pytest.public_cert_bytes
+            + pytest.public_cert_bytes,
+            pytest.public_cert_bytes + pytest.public_cert_bytes,
+            pytest.private_key_bytes,
+        )
+
+    def test_key(self):
+        # Create some fake keys for regex check.
+        KEY = b"""-----BEGIN PRIVATE KEY-----
+        MIIBCgKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj7wZg
+        /fy3ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQAB
+        -----END PRIVATE KEY-----"""
+        RSA_KEY = b"""-----BEGIN RSA PRIVATE KEY-----
+        MIIBCgKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj7wZg
+        /fy3ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQAB
+        -----END RSA PRIVATE KEY-----"""
+        EC_KEY = b"""-----BEGIN EC PRIVATE KEY-----
+        MIIBCgKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj7wZg
+        /fy3ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQAB
+        -----END EC PRIVATE KEY-----"""
+
+        check_cert_and_key(
+            pytest.public_cert_bytes + KEY, pytest.public_cert_bytes, KEY
+        )
+        check_cert_and_key(
+            pytest.public_cert_bytes + RSA_KEY, pytest.public_cert_bytes, RSA_KEY
+        )
+        check_cert_and_key(
+            pytest.public_cert_bytes + EC_KEY, pytest.public_cert_bytes, EC_KEY
+        )
+
+
+class TestCheckaMetadataPath(object):
+    def test_success(self):
+        metadata_path = os.path.join(pytest.data_dir, "context_aware_metadata.json")
+        returned_path = _mtls_helper._check_dca_metadata_path(metadata_path)
+        assert returned_path is not None
+
+    def test_failure(self):
+        metadata_path = os.path.join(pytest.data_dir, "not_exists.json")
+        returned_path = _mtls_helper._check_dca_metadata_path(metadata_path)
+        assert returned_path is None
+
+
+class TestReadMetadataFile(object):
+    def test_success(self):
+        metadata_path = os.path.join(pytest.data_dir, "context_aware_metadata.json")
+        metadata = _mtls_helper._read_dca_metadata_file(metadata_path)
+
+        assert "cert_provider_command" in metadata
+
+    def test_file_not_json(self):
+        # read a file which is not json format.
+        metadata_path = os.path.join(pytest.data_dir, "privatekey.pem")
+        with pytest.raises(exceptions.ClientCertError):
+            _mtls_helper._read_dca_metadata_file(metadata_path)
+
+
+class TestRunCertProviderCommand(object):
+    def create_mock_process(self, output, error):
+        # There are two steps to execute a script with subprocess.Popen.
+        # (1) process = subprocess.Popen([comannds])
+        # (2) stdout, stderr = process.communicate()
+        # This function creates a mock process which can be returned by a mock
+        # subprocess.Popen. The mock process returns the given output and error
+        # when mock_process.communicate() is called.
+        mock_process = mock.Mock()
+        attrs = {"communicate.return_value": (output, error), "returncode": 0}
+        mock_process.configure_mock(**attrs)
+        return mock_process
+
+    @mock.patch("subprocess.Popen", autospec=True)
+    def test_success(self, mock_popen):
+        mock_popen.return_value = self.create_mock_process(
+            pytest.public_cert_bytes + pytest.private_key_bytes, b""
+        )
+        cert, key, passphrase = _mtls_helper._run_cert_provider_command(["command"])
+        assert cert == pytest.public_cert_bytes
+        assert key == pytest.private_key_bytes
+        assert passphrase is None
+
+        mock_popen.return_value = self.create_mock_process(
+            pytest.public_cert_bytes + ENCRYPTED_EC_PRIVATE_KEY + PASSPHRASE, b""
+        )
+        cert, key, passphrase = _mtls_helper._run_cert_provider_command(
+            ["command"], expect_encrypted_key=True
+        )
+        assert cert == pytest.public_cert_bytes
+        assert key == ENCRYPTED_EC_PRIVATE_KEY
+        assert passphrase == PASSPHRASE_VALUE
+
+    @mock.patch("subprocess.Popen", autospec=True)
+    def test_success_with_cert_chain(self, mock_popen):
+        PUBLIC_CERT_CHAIN_BYTES = pytest.public_cert_bytes + pytest.public_cert_bytes
+        mock_popen.return_value = self.create_mock_process(
+            PUBLIC_CERT_CHAIN_BYTES + pytest.private_key_bytes, b""
+        )
+        cert, key, passphrase = _mtls_helper._run_cert_provider_command(["command"])
+        assert cert == PUBLIC_CERT_CHAIN_BYTES
+        assert key == pytest.private_key_bytes
+        assert passphrase is None
+
+        mock_popen.return_value = self.create_mock_process(
+            PUBLIC_CERT_CHAIN_BYTES + ENCRYPTED_EC_PRIVATE_KEY + PASSPHRASE, b""
+        )
+        cert, key, passphrase = _mtls_helper._run_cert_provider_command(
+            ["command"], expect_encrypted_key=True
+        )
+        assert cert == PUBLIC_CERT_CHAIN_BYTES
+        assert key == ENCRYPTED_EC_PRIVATE_KEY
+        assert passphrase == PASSPHRASE_VALUE
+
+    @mock.patch("subprocess.Popen", autospec=True)
+    def test_missing_cert(self, mock_popen):
+        mock_popen.return_value = self.create_mock_process(
+            pytest.private_key_bytes, b""
+        )
+        with pytest.raises(exceptions.ClientCertError):
+            _mtls_helper._run_cert_provider_command(["command"])
+
+        mock_popen.return_value = self.create_mock_process(
+            ENCRYPTED_EC_PRIVATE_KEY + PASSPHRASE, b""
+        )
+        with pytest.raises(exceptions.ClientCertError):
+            _mtls_helper._run_cert_provider_command(
+                ["command"], expect_encrypted_key=True
+            )
+
+    @mock.patch("subprocess.Popen", autospec=True)
+    def test_missing_key(self, mock_popen):
+        mock_popen.return_value = self.create_mock_process(
+            pytest.public_cert_bytes, b""
+        )
+        with pytest.raises(exceptions.ClientCertError):
+            _mtls_helper._run_cert_provider_command(["command"])
+
+        mock_popen.return_value = self.create_mock_process(
+            pytest.public_cert_bytes + PASSPHRASE, b""
+        )
+        with pytest.raises(exceptions.ClientCertError):
+            _mtls_helper._run_cert_provider_command(
+                ["command"], expect_encrypted_key=True
+            )
+
+    @mock.patch("subprocess.Popen", autospec=True)
+    def test_missing_passphrase(self, mock_popen):
+        mock_popen.return_value = self.create_mock_process(
+            pytest.public_cert_bytes + ENCRYPTED_EC_PRIVATE_KEY, b""
+        )
+        with pytest.raises(exceptions.ClientCertError):
+            _mtls_helper._run_cert_provider_command(
+                ["command"], expect_encrypted_key=True
+            )
+
+    @mock.patch("subprocess.Popen", autospec=True)
+    def test_passphrase_not_expected(self, mock_popen):
+        mock_popen.return_value = self.create_mock_process(
+            pytest.public_cert_bytes + pytest.private_key_bytes + PASSPHRASE, b""
+        )
+        with pytest.raises(exceptions.ClientCertError):
+            _mtls_helper._run_cert_provider_command(["command"])
+
+    @mock.patch("subprocess.Popen", autospec=True)
+    def test_encrypted_key_expected(self, mock_popen):
+        mock_popen.return_value = self.create_mock_process(
+            pytest.public_cert_bytes + pytest.private_key_bytes + PASSPHRASE, b""
+        )
+        with pytest.raises(exceptions.ClientCertError):
+            _mtls_helper._run_cert_provider_command(
+                ["command"], expect_encrypted_key=True
+            )
+
+    @mock.patch("subprocess.Popen", autospec=True)
+    def test_unencrypted_key_expected(self, mock_popen):
+        mock_popen.return_value = self.create_mock_process(
+            pytest.public_cert_bytes + ENCRYPTED_EC_PRIVATE_KEY, b""
+        )
+        with pytest.raises(exceptions.ClientCertError):
+            _mtls_helper._run_cert_provider_command(["command"])
+
+    @mock.patch("subprocess.Popen", autospec=True)
+    def test_cert_provider_returns_error(self, mock_popen):
+        mock_popen.return_value = self.create_mock_process(b"", b"some error")
+        mock_popen.return_value.returncode = 1
+        with pytest.raises(exceptions.ClientCertError):
+            _mtls_helper._run_cert_provider_command(["command"])
+
+    @mock.patch("subprocess.Popen", autospec=True)
+    def test_popen_raise_exception(self, mock_popen):
+        mock_popen.side_effect = OSError()
+        with pytest.raises(exceptions.ClientCertError):
+            _mtls_helper._run_cert_provider_command(["command"])
+
+
+class TestGetClientSslCredentials(object):
+    @mock.patch(
+        "google.auth.transport._mtls_helper._run_cert_provider_command", autospec=True
+    )
+    @mock.patch(
+        "google.auth.transport._mtls_helper._read_dca_metadata_file", autospec=True
+    )
+    @mock.patch(
+        "google.auth.transport._mtls_helper._check_dca_metadata_path", autospec=True
+    )
+    def test_success(
+        self,
+        mock_check_dca_metadata_path,
+        mock_read_dca_metadata_file,
+        mock_run_cert_provider_command,
+    ):
+        mock_check_dca_metadata_path.return_value = True
+        mock_read_dca_metadata_file.return_value = {
+            "cert_provider_command": ["command"]
+        }
+        mock_run_cert_provider_command.return_value = (b"cert", b"key", None)
+        has_cert, cert, key, passphrase = _mtls_helper.get_client_ssl_credentials()
+        assert has_cert
+        assert cert == b"cert"
+        assert key == b"key"
+        assert passphrase is None
+
+    @mock.patch(
+        "google.auth.transport._mtls_helper._check_dca_metadata_path", autospec=True
+    )
+    def test_success_without_metadata(self, mock_check_dca_metadata_path):
+        mock_check_dca_metadata_path.return_value = False
+        has_cert, cert, key, passphrase = _mtls_helper.get_client_ssl_credentials()
+        assert not has_cert
+        assert cert is None
+        assert key is None
+        assert passphrase is None
+
+    @mock.patch(
+        "google.auth.transport._mtls_helper._run_cert_provider_command", autospec=True
+    )
+    @mock.patch(
+        "google.auth.transport._mtls_helper._read_dca_metadata_file", autospec=True
+    )
+    @mock.patch(
+        "google.auth.transport._mtls_helper._check_dca_metadata_path", autospec=True
+    )
+    def test_success_with_encrypted_key(
+        self,
+        mock_check_dca_metadata_path,
+        mock_read_dca_metadata_file,
+        mock_run_cert_provider_command,
+    ):
+        mock_check_dca_metadata_path.return_value = True
+        mock_read_dca_metadata_file.return_value = {
+            "cert_provider_command": ["command"]
+        }
+        mock_run_cert_provider_command.return_value = (b"cert", b"key", b"passphrase")
+        has_cert, cert, key, passphrase = _mtls_helper.get_client_ssl_credentials(
+            generate_encrypted_key=True
+        )
+        assert has_cert
+        assert cert == b"cert"
+        assert key == b"key"
+        assert passphrase == b"passphrase"
+        mock_run_cert_provider_command.assert_called_once_with(
+            ["command", "--with_passphrase"], expect_encrypted_key=True
+        )
+
+    @mock.patch(
+        "google.auth.transport._mtls_helper._read_dca_metadata_file", autospec=True
+    )
+    @mock.patch(
+        "google.auth.transport._mtls_helper._check_dca_metadata_path", autospec=True
+    )
+    def test_missing_cert_command(
+        self, mock_check_dca_metadata_path, mock_read_dca_metadata_file
+    ):
+        mock_check_dca_metadata_path.return_value = True
+        mock_read_dca_metadata_file.return_value = {}
+        with pytest.raises(exceptions.ClientCertError):
+            _mtls_helper.get_client_ssl_credentials()
+
+    @mock.patch(
+        "google.auth.transport._mtls_helper._run_cert_provider_command", autospec=True
+    )
+    @mock.patch(
+        "google.auth.transport._mtls_helper._read_dca_metadata_file", autospec=True
+    )
+    @mock.patch(
+        "google.auth.transport._mtls_helper._check_dca_metadata_path", autospec=True
+    )
+    def test_customize_context_aware_metadata_path(
+        self,
+        mock_check_dca_metadata_path,
+        mock_read_dca_metadata_file,
+        mock_run_cert_provider_command,
+    ):
+        context_aware_metadata_path = "/path/to/metata/data"
+        mock_check_dca_metadata_path.return_value = context_aware_metadata_path
+        mock_read_dca_metadata_file.return_value = {
+            "cert_provider_command": ["command"]
+        }
+        mock_run_cert_provider_command.return_value = (b"cert", b"key", None)
+
+        has_cert, cert, key, passphrase = _mtls_helper.get_client_ssl_credentials(
+            context_aware_metadata_path=context_aware_metadata_path
+        )
+
+        assert has_cert
+        assert cert == b"cert"
+        assert key == b"key"
+        assert passphrase is None
+        mock_check_dca_metadata_path.assert_called_with(context_aware_metadata_path)
+        mock_read_dca_metadata_file.assert_called_with(context_aware_metadata_path)
+
+
+class TestGetClientCertAndKey(object):
+    def test_callback_success(self):
+        callback = mock.Mock()
+        callback.return_value = (pytest.public_cert_bytes, pytest.private_key_bytes)
+
+        found_cert_key, cert, key = _mtls_helper.get_client_cert_and_key(callback)
+        assert found_cert_key
+        assert cert == pytest.public_cert_bytes
+        assert key == pytest.private_key_bytes
+
+    @mock.patch(
+        "google.auth.transport._mtls_helper.get_client_ssl_credentials", autospec=True
+    )
+    def test_use_metadata(self, mock_get_client_ssl_credentials):
+        mock_get_client_ssl_credentials.return_value = (
+            True,
+            pytest.public_cert_bytes,
+            pytest.private_key_bytes,
+            None,
+        )
+
+        found_cert_key, cert, key = _mtls_helper.get_client_cert_and_key()
+        assert found_cert_key
+        assert cert == pytest.public_cert_bytes
+        assert key == pytest.private_key_bytes
+
+
+class TestDecryptPrivateKey(object):
+    def test_success(self):
+        decrypted_key = _mtls_helper.decrypt_private_key(
+            ENCRYPTED_EC_PRIVATE_KEY, PASSPHRASE_VALUE
+        )
+        private_key = crypto.load_privatekey(crypto.FILETYPE_PEM, decrypted_key)
+        public_key = crypto.load_publickey(crypto.FILETYPE_PEM, EC_PUBLIC_KEY)
+        x509 = crypto.X509()
+        x509.set_pubkey(public_key)
+
+        # Test the decrypted key works by signing and verification.
+        signature = crypto.sign(private_key, b"data", "sha256")
+        crypto.verify(x509, signature, b"data", "sha256")
+
+    def test_crypto_error(self):
+        with pytest.raises(crypto.Error):
+            _mtls_helper.decrypt_private_key(
+                ENCRYPTED_EC_PRIVATE_KEY, b"wrong_password"
+            )
diff --git a/tests/transport/test_grpc.py b/tests/transport/test_grpc.py
new file mode 100644
index 0000000..3437658
--- /dev/null
+++ b/tests/transport/test_grpc.py
@@ -0,0 +1,502 @@
+# Copyright 2016 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 datetime
+import os
+import time
+
+import mock
+import pytest
+
+from google.auth import _helpers
+from google.auth import credentials
+from google.auth import environment_vars
+from google.auth import exceptions
+from google.auth import transport
+from google.oauth2 import service_account
+
+try:
+    # pylint: disable=ungrouped-imports
+    import grpc
+    import google.auth.transport.grpc
+
+    HAS_GRPC = True
+except ImportError:  # pragma: NO COVER
+    HAS_GRPC = False
+
+DATA_DIR = os.path.join(os.path.dirname(__file__), "..", "data")
+METADATA_PATH = os.path.join(DATA_DIR, "context_aware_metadata.json")
+with open(os.path.join(DATA_DIR, "privatekey.pem"), "rb") as fh:
+    PRIVATE_KEY_BYTES = fh.read()
+with open(os.path.join(DATA_DIR, "public_cert.pem"), "rb") as fh:
+    PUBLIC_CERT_BYTES = fh.read()
+
+pytestmark = pytest.mark.skipif(not HAS_GRPC, reason="gRPC is unavailable.")
+
+
+class CredentialsStub(credentials.Credentials):
+    def __init__(self, token="token"):
+        super(CredentialsStub, self).__init__()
+        self.token = token
+        self.expiry = None
+
+    def refresh(self, request):
+        self.token += "1"
+
+    def with_quota_project(self, quota_project_id):
+        raise NotImplementedError()
+
+
+class TestAuthMetadataPlugin(object):
+    def test_call_no_refresh(self):
+        credentials = CredentialsStub()
+        request = mock.create_autospec(transport.Request)
+
+        plugin = google.auth.transport.grpc.AuthMetadataPlugin(credentials, request)
+
+        context = mock.create_autospec(grpc.AuthMetadataContext, instance=True)
+        context.method_name = mock.sentinel.method_name
+        context.service_url = mock.sentinel.service_url
+        callback = mock.create_autospec(grpc.AuthMetadataPluginCallback)
+
+        plugin(context, callback)
+
+        time.sleep(2)
+
+        callback.assert_called_once_with(
+            [("authorization", "Bearer {}".format(credentials.token))], None
+        )
+
+    def test_call_refresh(self):
+        credentials = CredentialsStub()
+        credentials.expiry = datetime.datetime.min + _helpers.REFRESH_THRESHOLD
+        request = mock.create_autospec(transport.Request)
+
+        plugin = google.auth.transport.grpc.AuthMetadataPlugin(credentials, request)
+
+        context = mock.create_autospec(grpc.AuthMetadataContext, instance=True)
+        context.method_name = mock.sentinel.method_name
+        context.service_url = mock.sentinel.service_url
+        callback = mock.create_autospec(grpc.AuthMetadataPluginCallback)
+
+        plugin(context, callback)
+
+        time.sleep(2)
+
+        assert credentials.token == "token1"
+        callback.assert_called_once_with(
+            [("authorization", "Bearer {}".format(credentials.token))], None
+        )
+
+    def test__get_authorization_headers_with_service_account(self):
+        credentials = mock.create_autospec(service_account.Credentials)
+        request = mock.create_autospec(transport.Request)
+
+        plugin = google.auth.transport.grpc.AuthMetadataPlugin(credentials, request)
+
+        context = mock.create_autospec(grpc.AuthMetadataContext, instance=True)
+        context.method_name = "methodName"
+        context.service_url = "https://pubsub.googleapis.com/methodName"
+
+        plugin._get_authorization_headers(context)
+
+        credentials._create_self_signed_jwt.assert_called_once_with(None)
+
+    def test__get_authorization_headers_with_service_account_and_default_host(self):
+        credentials = mock.create_autospec(service_account.Credentials)
+        request = mock.create_autospec(transport.Request)
+
+        default_host = "pubsub.googleapis.com"
+        plugin = google.auth.transport.grpc.AuthMetadataPlugin(
+            credentials, request, default_host=default_host
+        )
+
+        context = mock.create_autospec(grpc.AuthMetadataContext, instance=True)
+        context.method_name = "methodName"
+        context.service_url = "https://pubsub.googleapis.com/methodName"
+
+        plugin._get_authorization_headers(context)
+
+        credentials._create_self_signed_jwt.assert_called_once_with(
+            "https://{}/".format(default_host)
+        )
+
+
[email protected](
+    "google.auth.transport._mtls_helper.get_client_ssl_credentials", autospec=True
+)
[email protected]("grpc.composite_channel_credentials", autospec=True)
[email protected]("grpc.metadata_call_credentials", autospec=True)
[email protected]("grpc.ssl_channel_credentials", autospec=True)
[email protected]("grpc.secure_channel", autospec=True)
+class TestSecureAuthorizedChannel(object):
+    @mock.patch(
+        "google.auth.transport._mtls_helper._read_dca_metadata_file", autospec=True
+    )
+    @mock.patch(
+        "google.auth.transport._mtls_helper._check_dca_metadata_path", autospec=True
+    )
+    def test_secure_authorized_channel_adc(
+        self,
+        check_dca_metadata_path,
+        read_dca_metadata_file,
+        secure_channel,
+        ssl_channel_credentials,
+        metadata_call_credentials,
+        composite_channel_credentials,
+        get_client_ssl_credentials,
+    ):
+        credentials = CredentialsStub()
+        request = mock.create_autospec(transport.Request)
+        target = "example.com:80"
+
+        # Mock the context aware metadata and client cert/key so mTLS SSL channel
+        # will be used.
+        check_dca_metadata_path.return_value = METADATA_PATH
+        read_dca_metadata_file.return_value = {
+            "cert_provider_command": ["some command"]
+        }
+        get_client_ssl_credentials.return_value = (
+            True,
+            PUBLIC_CERT_BYTES,
+            PRIVATE_KEY_BYTES,
+            None,
+        )
+
+        channel = None
+        with mock.patch.dict(
+            os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}
+        ):
+            channel = google.auth.transport.grpc.secure_authorized_channel(
+                credentials, request, target, options=mock.sentinel.options
+            )
+
+        # Check the auth plugin construction.
+        auth_plugin = metadata_call_credentials.call_args[0][0]
+        assert isinstance(auth_plugin, google.auth.transport.grpc.AuthMetadataPlugin)
+        assert auth_plugin._credentials == credentials
+        assert auth_plugin._request == request
+
+        # Check the ssl channel call.
+        ssl_channel_credentials.assert_called_once_with(
+            certificate_chain=PUBLIC_CERT_BYTES, private_key=PRIVATE_KEY_BYTES
+        )
+
+        # Check the composite credentials call.
+        composite_channel_credentials.assert_called_once_with(
+            ssl_channel_credentials.return_value, metadata_call_credentials.return_value
+        )
+
+        # Check the channel call.
+        secure_channel.assert_called_once_with(
+            target,
+            composite_channel_credentials.return_value,
+            options=mock.sentinel.options,
+        )
+        assert channel == secure_channel.return_value
+
+    @mock.patch("google.auth.transport.grpc.SslCredentials", autospec=True)
+    def test_secure_authorized_channel_adc_without_client_cert_env(
+        self,
+        ssl_credentials_adc_method,
+        secure_channel,
+        ssl_channel_credentials,
+        metadata_call_credentials,
+        composite_channel_credentials,
+        get_client_ssl_credentials,
+    ):
+        # Test client cert won't be used if GOOGLE_API_USE_CLIENT_CERTIFICATE
+        # environment variable is not set.
+        credentials = CredentialsStub()
+        request = mock.create_autospec(transport.Request)
+        target = "example.com:80"
+
+        channel = google.auth.transport.grpc.secure_authorized_channel(
+            credentials, request, target, options=mock.sentinel.options
+        )
+
+        # Check the auth plugin construction.
+        auth_plugin = metadata_call_credentials.call_args[0][0]
+        assert isinstance(auth_plugin, google.auth.transport.grpc.AuthMetadataPlugin)
+        assert auth_plugin._credentials == credentials
+        assert auth_plugin._request == request
+
+        # Check the ssl channel call.
+        ssl_channel_credentials.assert_called_once()
+        ssl_credentials_adc_method.assert_not_called()
+
+        # Check the composite credentials call.
+        composite_channel_credentials.assert_called_once_with(
+            ssl_channel_credentials.return_value, metadata_call_credentials.return_value
+        )
+
+        # Check the channel call.
+        secure_channel.assert_called_once_with(
+            target,
+            composite_channel_credentials.return_value,
+            options=mock.sentinel.options,
+        )
+        assert channel == secure_channel.return_value
+
+    def test_secure_authorized_channel_explicit_ssl(
+        self,
+        secure_channel,
+        ssl_channel_credentials,
+        metadata_call_credentials,
+        composite_channel_credentials,
+        get_client_ssl_credentials,
+    ):
+        credentials = mock.Mock()
+        request = mock.Mock()
+        target = "example.com:80"
+        ssl_credentials = mock.Mock()
+
+        google.auth.transport.grpc.secure_authorized_channel(
+            credentials, request, target, ssl_credentials=ssl_credentials
+        )
+
+        # Since explicit SSL credentials are provided, get_client_ssl_credentials
+        # shouldn't be called.
+        assert not get_client_ssl_credentials.called
+
+        # Check the ssl channel call.
+        assert not ssl_channel_credentials.called
+
+        # Check the composite credentials call.
+        composite_channel_credentials.assert_called_once_with(
+            ssl_credentials, metadata_call_credentials.return_value
+        )
+
+    def test_secure_authorized_channel_mutual_exclusive(
+        self,
+        secure_channel,
+        ssl_channel_credentials,
+        metadata_call_credentials,
+        composite_channel_credentials,
+        get_client_ssl_credentials,
+    ):
+        credentials = mock.Mock()
+        request = mock.Mock()
+        target = "example.com:80"
+        ssl_credentials = mock.Mock()
+        client_cert_callback = mock.Mock()
+
+        with pytest.raises(ValueError):
+            google.auth.transport.grpc.secure_authorized_channel(
+                credentials,
+                request,
+                target,
+                ssl_credentials=ssl_credentials,
+                client_cert_callback=client_cert_callback,
+            )
+
+    def test_secure_authorized_channel_with_client_cert_callback_success(
+        self,
+        secure_channel,
+        ssl_channel_credentials,
+        metadata_call_credentials,
+        composite_channel_credentials,
+        get_client_ssl_credentials,
+    ):
+        credentials = mock.Mock()
+        request = mock.Mock()
+        target = "example.com:80"
+        client_cert_callback = mock.Mock()
+        client_cert_callback.return_value = (PUBLIC_CERT_BYTES, PRIVATE_KEY_BYTES)
+
+        with mock.patch.dict(
+            os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}
+        ):
+            google.auth.transport.grpc.secure_authorized_channel(
+                credentials, request, target, client_cert_callback=client_cert_callback
+            )
+
+        client_cert_callback.assert_called_once()
+
+        # Check we are using the cert and key provided by client_cert_callback.
+        ssl_channel_credentials.assert_called_once_with(
+            certificate_chain=PUBLIC_CERT_BYTES, private_key=PRIVATE_KEY_BYTES
+        )
+
+        # Check the composite credentials call.
+        composite_channel_credentials.assert_called_once_with(
+            ssl_channel_credentials.return_value, metadata_call_credentials.return_value
+        )
+
+    @mock.patch(
+        "google.auth.transport._mtls_helper._read_dca_metadata_file", autospec=True
+    )
+    @mock.patch(
+        "google.auth.transport._mtls_helper._check_dca_metadata_path", autospec=True
+    )
+    def test_secure_authorized_channel_with_client_cert_callback_failure(
+        self,
+        check_dca_metadata_path,
+        read_dca_metadata_file,
+        secure_channel,
+        ssl_channel_credentials,
+        metadata_call_credentials,
+        composite_channel_credentials,
+        get_client_ssl_credentials,
+    ):
+        credentials = mock.Mock()
+        request = mock.Mock()
+        target = "example.com:80"
+
+        client_cert_callback = mock.Mock()
+        client_cert_callback.side_effect = Exception("callback exception")
+
+        with pytest.raises(Exception) as excinfo:
+            with mock.patch.dict(
+                os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}
+            ):
+                google.auth.transport.grpc.secure_authorized_channel(
+                    credentials,
+                    request,
+                    target,
+                    client_cert_callback=client_cert_callback,
+                )
+
+        assert str(excinfo.value) == "callback exception"
+
+    def test_secure_authorized_channel_cert_callback_without_client_cert_env(
+        self,
+        secure_channel,
+        ssl_channel_credentials,
+        metadata_call_credentials,
+        composite_channel_credentials,
+        get_client_ssl_credentials,
+    ):
+        # Test client cert won't be used if GOOGLE_API_USE_CLIENT_CERTIFICATE
+        # environment variable is not set.
+        credentials = mock.Mock()
+        request = mock.Mock()
+        target = "example.com:80"
+        client_cert_callback = mock.Mock()
+
+        google.auth.transport.grpc.secure_authorized_channel(
+            credentials, request, target, client_cert_callback=client_cert_callback
+        )
+
+        # Check client_cert_callback is not called because GOOGLE_API_USE_CLIENT_CERTIFICATE
+        # is not set.
+        client_cert_callback.assert_not_called()
+
+        ssl_channel_credentials.assert_called_once()
+
+        # Check the composite credentials call.
+        composite_channel_credentials.assert_called_once_with(
+            ssl_channel_credentials.return_value, metadata_call_credentials.return_value
+        )
+
+
[email protected]("grpc.ssl_channel_credentials", autospec=True)
[email protected](
+    "google.auth.transport._mtls_helper.get_client_ssl_credentials", autospec=True
+)
[email protected]("google.auth.transport._mtls_helper._read_dca_metadata_file", autospec=True)
[email protected](
+    "google.auth.transport._mtls_helper._check_dca_metadata_path", autospec=True
+)
+class TestSslCredentials(object):
+    def test_no_context_aware_metadata(
+        self,
+        mock_check_dca_metadata_path,
+        mock_read_dca_metadata_file,
+        mock_get_client_ssl_credentials,
+        mock_ssl_channel_credentials,
+    ):
+        # Mock that the metadata file doesn't exist.
+        mock_check_dca_metadata_path.return_value = None
+
+        with mock.patch.dict(
+            os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}
+        ):
+            ssl_credentials = google.auth.transport.grpc.SslCredentials()
+
+        # Since no context aware metadata is found, we wouldn't call
+        # get_client_ssl_credentials, and the SSL channel credentials created is
+        # non mTLS.
+        assert ssl_credentials.ssl_credentials is not None
+        assert not ssl_credentials.is_mtls
+        mock_get_client_ssl_credentials.assert_not_called()
+        mock_ssl_channel_credentials.assert_called_once_with()
+
+    def test_get_client_ssl_credentials_failure(
+        self,
+        mock_check_dca_metadata_path,
+        mock_read_dca_metadata_file,
+        mock_get_client_ssl_credentials,
+        mock_ssl_channel_credentials,
+    ):
+        mock_check_dca_metadata_path.return_value = METADATA_PATH
+        mock_read_dca_metadata_file.return_value = {
+            "cert_provider_command": ["some command"]
+        }
+
+        # Mock that client cert and key are not loaded and exception is raised.
+        mock_get_client_ssl_credentials.side_effect = exceptions.ClientCertError()
+
+        with pytest.raises(exceptions.MutualTLSChannelError):
+            with mock.patch.dict(
+                os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}
+            ):
+                assert google.auth.transport.grpc.SslCredentials().ssl_credentials
+
+    def test_get_client_ssl_credentials_success(
+        self,
+        mock_check_dca_metadata_path,
+        mock_read_dca_metadata_file,
+        mock_get_client_ssl_credentials,
+        mock_ssl_channel_credentials,
+    ):
+        mock_check_dca_metadata_path.return_value = METADATA_PATH
+        mock_read_dca_metadata_file.return_value = {
+            "cert_provider_command": ["some command"]
+        }
+        mock_get_client_ssl_credentials.return_value = (
+            True,
+            PUBLIC_CERT_BYTES,
+            PRIVATE_KEY_BYTES,
+            None,
+        )
+
+        with mock.patch.dict(
+            os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}
+        ):
+            ssl_credentials = google.auth.transport.grpc.SslCredentials()
+
+        assert ssl_credentials.ssl_credentials is not None
+        assert ssl_credentials.is_mtls
+        mock_get_client_ssl_credentials.assert_called_once()
+        mock_ssl_channel_credentials.assert_called_once_with(
+            certificate_chain=PUBLIC_CERT_BYTES, private_key=PRIVATE_KEY_BYTES
+        )
+
+    def test_get_client_ssl_credentials_without_client_cert_env(
+        self,
+        mock_check_dca_metadata_path,
+        mock_read_dca_metadata_file,
+        mock_get_client_ssl_credentials,
+        mock_ssl_channel_credentials,
+    ):
+        # Test client cert won't be used if GOOGLE_API_USE_CLIENT_CERTIFICATE is not set.
+        ssl_credentials = google.auth.transport.grpc.SslCredentials()
+
+        assert ssl_credentials.ssl_credentials is not None
+        assert not ssl_credentials.is_mtls
+        mock_check_dca_metadata_path.assert_not_called()
+        mock_read_dca_metadata_file.assert_not_called()
+        mock_get_client_ssl_credentials.assert_not_called()
+        mock_ssl_channel_credentials.assert_called_once()
diff --git a/tests/transport/test_mtls.py b/tests/transport/test_mtls.py
new file mode 100644
index 0000000..ff70bb3
--- /dev/null
+++ b/tests/transport/test_mtls.py
@@ -0,0 +1,83 @@
+# 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.
+
+import mock
+import pytest
+
+from google.auth import exceptions
+from google.auth.transport import mtls
+
+
[email protected](
+    "google.auth.transport._mtls_helper._check_dca_metadata_path", autospec=True
+)
+def test_has_default_client_cert_source(check_dca_metadata_path):
+    check_dca_metadata_path.return_value = mock.Mock()
+    assert mtls.has_default_client_cert_source()
+
+    check_dca_metadata_path.return_value = None
+    assert not mtls.has_default_client_cert_source()
+
+
[email protected]("google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True)
[email protected]("google.auth.transport.mtls.has_default_client_cert_source", autospec=True)
+def test_default_client_cert_source(
+    has_default_client_cert_source, get_client_cert_and_key
+):
+    # Test default client cert source doesn't exist.
+    has_default_client_cert_source.return_value = False
+    with pytest.raises(exceptions.MutualTLSChannelError):
+        mtls.default_client_cert_source()
+
+    # The following tests will assume default client cert source exists.
+    has_default_client_cert_source.return_value = True
+
+    # Test good callback.
+    get_client_cert_and_key.return_value = (True, b"cert", b"key")
+    callback = mtls.default_client_cert_source()
+    assert callback() == (b"cert", b"key")
+
+    # Test bad callback which throws exception.
+    get_client_cert_and_key.side_effect = ValueError()
+    callback = mtls.default_client_cert_source()
+    with pytest.raises(exceptions.MutualTLSChannelError):
+        callback()
+
+
[email protected](
+    "google.auth.transport._mtls_helper.get_client_ssl_credentials", autospec=True
+)
[email protected]("google.auth.transport.mtls.has_default_client_cert_source", autospec=True)
+def test_default_client_encrypted_cert_source(
+    has_default_client_cert_source, get_client_ssl_credentials
+):
+    # Test default client cert source doesn't exist.
+    has_default_client_cert_source.return_value = False
+    with pytest.raises(exceptions.MutualTLSChannelError):
+        mtls.default_client_encrypted_cert_source("cert_path", "key_path")
+
+    # The following tests will assume default client cert source exists.
+    has_default_client_cert_source.return_value = True
+
+    # Test good callback.
+    get_client_ssl_credentials.return_value = (True, b"cert", b"key", b"passphrase")
+    callback = mtls.default_client_encrypted_cert_source("cert_path", "key_path")
+    with mock.patch("{}.open".format(__name__), return_value=mock.MagicMock()):
+        assert callback() == ("cert_path", "key_path", b"passphrase")
+
+    # Test bad callback which throws exception.
+    get_client_ssl_credentials.side_effect = exceptions.ClientCertError()
+    callback = mtls.default_client_encrypted_cert_source("cert_path", "key_path")
+    with pytest.raises(exceptions.MutualTLSChannelError):
+        callback()
diff --git a/tests/transport/test_requests.py b/tests/transport/test_requests.py
new file mode 100644
index 0000000..ed9300d
--- /dev/null
+++ b/tests/transport/test_requests.py
@@ -0,0 +1,525 @@
+# Copyright 2016 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 datetime
+import functools
+import os
+import sys
+
+import freezegun
+import mock
+import OpenSSL
+import pytest
+import requests
+import requests.adapters
+from six.moves import http_client
+
+from google.auth import environment_vars
+from google.auth import exceptions
+import google.auth.credentials
+import google.auth.transport._mtls_helper
+import google.auth.transport.requests
+from google.oauth2 import service_account
+from tests.transport import compliance
+
+
[email protected]
+def frozen_time():
+    with freezegun.freeze_time("1970-01-01 00:00:00", tick=False) as frozen:
+        yield frozen
+
+
+class TestRequestResponse(compliance.RequestResponseTests):
+    def make_request(self):
+        return google.auth.transport.requests.Request()
+
+    def test_timeout(self):
+        http = mock.create_autospec(requests.Session, instance=True)
+        request = google.auth.transport.requests.Request(http)
+        request(url="http://example.com", method="GET", timeout=5)
+
+        assert http.request.call_args[1]["timeout"] == 5
+
+
+class TestTimeoutGuard(object):
+    def make_guard(self, *args, **kwargs):
+        return google.auth.transport.requests.TimeoutGuard(*args, **kwargs)
+
+    def test_tracks_elapsed_time_w_numeric_timeout(self, frozen_time):
+        with self.make_guard(timeout=10) as guard:
+            frozen_time.tick(delta=datetime.timedelta(seconds=3.8))
+        assert guard.remaining_timeout == 6.2
+
+    def test_tracks_elapsed_time_w_tuple_timeout(self, frozen_time):
+        with self.make_guard(timeout=(16, 19)) as guard:
+            frozen_time.tick(delta=datetime.timedelta(seconds=3.8))
+        assert guard.remaining_timeout == (12.2, 15.2)
+
+    def test_noop_if_no_timeout(self, frozen_time):
+        with self.make_guard(timeout=None) as guard:
+            frozen_time.tick(delta=datetime.timedelta(days=3650))
+        # NOTE: no timeout error raised, despite years have passed
+        assert guard.remaining_timeout is None
+
+    def test_timeout_error_w_numeric_timeout(self, frozen_time):
+        with pytest.raises(requests.exceptions.Timeout):
+            with self.make_guard(timeout=10) as guard:
+                frozen_time.tick(delta=datetime.timedelta(seconds=10.001))
+        assert guard.remaining_timeout == pytest.approx(-0.001)
+
+    def test_timeout_error_w_tuple_timeout(self, frozen_time):
+        with pytest.raises(requests.exceptions.Timeout):
+            with self.make_guard(timeout=(11, 10)) as guard:
+                frozen_time.tick(delta=datetime.timedelta(seconds=10.001))
+        assert guard.remaining_timeout == pytest.approx((0.999, -0.001))
+
+    def test_custom_timeout_error_type(self, frozen_time):
+        class FooError(Exception):
+            pass
+
+        with pytest.raises(FooError):
+            with self.make_guard(timeout=1, timeout_error_type=FooError):
+                frozen_time.tick(delta=datetime.timedelta(seconds=2))
+
+    def test_lets_suite_errors_bubble_up(self, frozen_time):
+        with pytest.raises(IndexError):
+            with self.make_guard(timeout=1):
+                [1, 2, 3][3]
+
+
+class CredentialsStub(google.auth.credentials.Credentials):
+    def __init__(self, token="token"):
+        super(CredentialsStub, self).__init__()
+        self.token = token
+
+    def apply(self, headers, token=None):
+        headers["authorization"] = self.token
+
+    def before_request(self, request, method, url, headers):
+        self.apply(headers)
+
+    def refresh(self, request):
+        self.token += "1"
+
+    def with_quota_project(self, quota_project_id):
+        raise NotImplementedError()
+
+
+class TimeTickCredentialsStub(CredentialsStub):
+    """Credentials that spend some (mocked) time when refreshing a token."""
+
+    def __init__(self, time_tick, token="token"):
+        self._time_tick = time_tick
+        super(TimeTickCredentialsStub, self).__init__(token=token)
+
+    def refresh(self, request):
+        self._time_tick()
+        super(TimeTickCredentialsStub, self).refresh(requests)
+
+
+class AdapterStub(requests.adapters.BaseAdapter):
+    def __init__(self, responses, headers=None):
+        super(AdapterStub, self).__init__()
+        self.responses = responses
+        self.requests = []
+        self.headers = headers or {}
+
+    def send(self, request, **kwargs):
+        # pylint: disable=arguments-differ
+        # request is the only required argument here and the only argument
+        # we care about.
+        self.requests.append(request)
+        return self.responses.pop(0)
+
+    def close(self):  # pragma: NO COVER
+        # pylint wants this to be here because it's abstract in the base
+        # class, but requests never actually calls it.
+        return
+
+
+class TimeTickAdapterStub(AdapterStub):
+    """Adapter that spends some (mocked) time when making a request."""
+
+    def __init__(self, time_tick, responses, headers=None):
+        self._time_tick = time_tick
+        super(TimeTickAdapterStub, self).__init__(responses, headers=headers)
+
+    def send(self, request, **kwargs):
+        self._time_tick()
+        return super(TimeTickAdapterStub, self).send(request, **kwargs)
+
+
+class TestMutualTlsAdapter(object):
+    @mock.patch.object(requests.adapters.HTTPAdapter, "init_poolmanager")
+    @mock.patch.object(requests.adapters.HTTPAdapter, "proxy_manager_for")
+    def test_success(self, mock_proxy_manager_for, mock_init_poolmanager):
+        adapter = google.auth.transport.requests._MutualTlsAdapter(
+            pytest.public_cert_bytes, pytest.private_key_bytes
+        )
+
+        adapter.init_poolmanager()
+        mock_init_poolmanager.assert_called_with(ssl_context=adapter._ctx_poolmanager)
+
+        adapter.proxy_manager_for()
+        mock_proxy_manager_for.assert_called_with(ssl_context=adapter._ctx_proxymanager)
+
+    def test_invalid_cert_or_key(self):
+        with pytest.raises(OpenSSL.crypto.Error):
+            google.auth.transport.requests._MutualTlsAdapter(
+                b"invalid cert", b"invalid key"
+            )
+
+    @mock.patch.dict("sys.modules", {"OpenSSL.crypto": None})
+    def test_import_error(self):
+        with pytest.raises(ImportError):
+            google.auth.transport.requests._MutualTlsAdapter(
+                pytest.public_cert_bytes, pytest.private_key_bytes
+            )
+
+
+def make_response(status=http_client.OK, data=None):
+    response = requests.Response()
+    response.status_code = status
+    response._content = data
+    return response
+
+
+class TestAuthorizedSession(object):
+    TEST_URL = "http://example.com/"
+
+    def test_constructor(self):
+        authed_session = google.auth.transport.requests.AuthorizedSession(
+            mock.sentinel.credentials
+        )
+
+        assert authed_session.credentials == mock.sentinel.credentials
+
+    def test_constructor_with_auth_request(self):
+        http = mock.create_autospec(requests.Session)
+        auth_request = google.auth.transport.requests.Request(http)
+
+        authed_session = google.auth.transport.requests.AuthorizedSession(
+            mock.sentinel.credentials, auth_request=auth_request
+        )
+
+        assert authed_session._auth_request is auth_request
+
+    def test_request_default_timeout(self):
+        credentials = mock.Mock(wraps=CredentialsStub())
+        response = make_response()
+        adapter = AdapterStub([response])
+
+        authed_session = google.auth.transport.requests.AuthorizedSession(credentials)
+        authed_session.mount(self.TEST_URL, adapter)
+
+        patcher = mock.patch("google.auth.transport.requests.requests.Session.request")
+        with patcher as patched_request:
+            authed_session.request("GET", self.TEST_URL)
+
+        expected_timeout = google.auth.transport.requests._DEFAULT_TIMEOUT
+        assert patched_request.call_args[1]["timeout"] == expected_timeout
+
+    def test_request_no_refresh(self):
+        credentials = mock.Mock(wraps=CredentialsStub())
+        response = make_response()
+        adapter = AdapterStub([response])
+
+        authed_session = google.auth.transport.requests.AuthorizedSession(credentials)
+        authed_session.mount(self.TEST_URL, adapter)
+
+        result = authed_session.request("GET", self.TEST_URL)
+
+        assert response == result
+        assert credentials.before_request.called
+        assert not credentials.refresh.called
+        assert len(adapter.requests) == 1
+        assert adapter.requests[0].url == self.TEST_URL
+        assert adapter.requests[0].headers["authorization"] == "token"
+
+    def test_request_refresh(self):
+        credentials = mock.Mock(wraps=CredentialsStub())
+        final_response = make_response(status=http_client.OK)
+        # First request will 401, second request will succeed.
+        adapter = AdapterStub(
+            [make_response(status=http_client.UNAUTHORIZED), final_response]
+        )
+
+        authed_session = google.auth.transport.requests.AuthorizedSession(
+            credentials, refresh_timeout=60
+        )
+        authed_session.mount(self.TEST_URL, adapter)
+
+        result = authed_session.request("GET", self.TEST_URL)
+
+        assert result == final_response
+        assert credentials.before_request.call_count == 2
+        assert credentials.refresh.called
+        assert len(adapter.requests) == 2
+
+        assert adapter.requests[0].url == self.TEST_URL
+        assert adapter.requests[0].headers["authorization"] == "token"
+
+        assert adapter.requests[1].url == self.TEST_URL
+        assert adapter.requests[1].headers["authorization"] == "token1"
+
+    def test_request_max_allowed_time_timeout_error(self, frozen_time):
+        tick_one_second = functools.partial(
+            frozen_time.tick, delta=datetime.timedelta(seconds=1.0)
+        )
+
+        credentials = mock.Mock(
+            wraps=TimeTickCredentialsStub(time_tick=tick_one_second)
+        )
+        adapter = TimeTickAdapterStub(
+            time_tick=tick_one_second, responses=[make_response(status=http_client.OK)]
+        )
+
+        authed_session = google.auth.transport.requests.AuthorizedSession(credentials)
+        authed_session.mount(self.TEST_URL, adapter)
+
+        # Because a request takes a full mocked second, max_allowed_time shorter
+        # than that will cause a timeout error.
+        with pytest.raises(requests.exceptions.Timeout):
+            authed_session.request("GET", self.TEST_URL, max_allowed_time=0.9)
+
+    def test_request_max_allowed_time_w_transport_timeout_no_error(self, frozen_time):
+        tick_one_second = functools.partial(
+            frozen_time.tick, delta=datetime.timedelta(seconds=1.0)
+        )
+
+        credentials = mock.Mock(
+            wraps=TimeTickCredentialsStub(time_tick=tick_one_second)
+        )
+        adapter = TimeTickAdapterStub(
+            time_tick=tick_one_second,
+            responses=[
+                make_response(status=http_client.UNAUTHORIZED),
+                make_response(status=http_client.OK),
+            ],
+        )
+
+        authed_session = google.auth.transport.requests.AuthorizedSession(credentials)
+        authed_session.mount(self.TEST_URL, adapter)
+
+        # A short configured transport timeout does not affect max_allowed_time.
+        # The latter is not adjusted to it and is only concerned with the actual
+        # execution time. The call below should thus not raise a timeout error.
+        authed_session.request("GET", self.TEST_URL, timeout=0.5, max_allowed_time=3.1)
+
+    def test_request_max_allowed_time_w_refresh_timeout_no_error(self, frozen_time):
+        tick_one_second = functools.partial(
+            frozen_time.tick, delta=datetime.timedelta(seconds=1.0)
+        )
+
+        credentials = mock.Mock(
+            wraps=TimeTickCredentialsStub(time_tick=tick_one_second)
+        )
+        adapter = TimeTickAdapterStub(
+            time_tick=tick_one_second,
+            responses=[
+                make_response(status=http_client.UNAUTHORIZED),
+                make_response(status=http_client.OK),
+            ],
+        )
+
+        authed_session = google.auth.transport.requests.AuthorizedSession(
+            credentials, refresh_timeout=1.1
+        )
+        authed_session.mount(self.TEST_URL, adapter)
+
+        # A short configured refresh timeout does not affect max_allowed_time.
+        # The latter is not adjusted to it and is only concerned with the actual
+        # execution time. The call below should thus not raise a timeout error
+        # (and `timeout` does not come into play either, as it's very long).
+        authed_session.request("GET", self.TEST_URL, timeout=60, max_allowed_time=3.1)
+
+    def test_request_timeout_w_refresh_timeout_timeout_error(self, frozen_time):
+        tick_one_second = functools.partial(
+            frozen_time.tick, delta=datetime.timedelta(seconds=1.0)
+        )
+
+        credentials = mock.Mock(
+            wraps=TimeTickCredentialsStub(time_tick=tick_one_second)
+        )
+        adapter = TimeTickAdapterStub(
+            time_tick=tick_one_second,
+            responses=[
+                make_response(status=http_client.UNAUTHORIZED),
+                make_response(status=http_client.OK),
+            ],
+        )
+
+        authed_session = google.auth.transport.requests.AuthorizedSession(
+            credentials, refresh_timeout=100
+        )
+        authed_session.mount(self.TEST_URL, adapter)
+
+        # An UNAUTHORIZED response triggers a refresh (an extra request), thus
+        # the final request that otherwise succeeds results in a timeout error
+        # (all three requests together last 3 mocked seconds).
+        with pytest.raises(requests.exceptions.Timeout):
+            authed_session.request(
+                "GET", self.TEST_URL, timeout=60, max_allowed_time=2.9
+            )
+
+    def test_authorized_session_without_default_host(self):
+        credentials = mock.create_autospec(service_account.Credentials)
+
+        authed_session = google.auth.transport.requests.AuthorizedSession(credentials)
+
+        authed_session.credentials._create_self_signed_jwt.assert_called_once_with(None)
+
+    def test_authorized_session_with_default_host(self):
+        default_host = "pubsub.googleapis.com"
+        credentials = mock.create_autospec(service_account.Credentials)
+
+        authed_session = google.auth.transport.requests.AuthorizedSession(
+            credentials, default_host=default_host
+        )
+
+        authed_session.credentials._create_self_signed_jwt.assert_called_once_with(
+            "https://{}/".format(default_host)
+        )
+
+    def test_configure_mtls_channel_with_callback(self):
+        mock_callback = mock.Mock()
+        mock_callback.return_value = (
+            pytest.public_cert_bytes,
+            pytest.private_key_bytes,
+        )
+
+        auth_session = google.auth.transport.requests.AuthorizedSession(
+            credentials=mock.Mock()
+        )
+        with mock.patch.dict(
+            os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}
+        ):
+            auth_session.configure_mtls_channel(mock_callback)
+
+        assert auth_session.is_mtls
+        assert isinstance(
+            auth_session.adapters["https://"],
+            google.auth.transport.requests._MutualTlsAdapter,
+        )
+
+    @mock.patch(
+        "google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True
+    )
+    def test_configure_mtls_channel_with_metadata(self, mock_get_client_cert_and_key):
+        mock_get_client_cert_and_key.return_value = (
+            True,
+            pytest.public_cert_bytes,
+            pytest.private_key_bytes,
+        )
+
+        auth_session = google.auth.transport.requests.AuthorizedSession(
+            credentials=mock.Mock()
+        )
+        with mock.patch.dict(
+            os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}
+        ):
+            auth_session.configure_mtls_channel()
+
+        assert auth_session.is_mtls
+        assert isinstance(
+            auth_session.adapters["https://"],
+            google.auth.transport.requests._MutualTlsAdapter,
+        )
+
+    @mock.patch.object(google.auth.transport.requests._MutualTlsAdapter, "__init__")
+    @mock.patch(
+        "google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True
+    )
+    def test_configure_mtls_channel_non_mtls(
+        self, mock_get_client_cert_and_key, mock_adapter_ctor
+    ):
+        mock_get_client_cert_and_key.return_value = (False, None, None)
+
+        auth_session = google.auth.transport.requests.AuthorizedSession(
+            credentials=mock.Mock()
+        )
+        with mock.patch.dict(
+            os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}
+        ):
+            auth_session.configure_mtls_channel()
+
+        assert not auth_session.is_mtls
+
+        # Assert _MutualTlsAdapter constructor is not called.
+        mock_adapter_ctor.assert_not_called()
+
+    @mock.patch(
+        "google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True
+    )
+    def test_configure_mtls_channel_exceptions(self, mock_get_client_cert_and_key):
+        mock_get_client_cert_and_key.side_effect = exceptions.ClientCertError()
+
+        auth_session = google.auth.transport.requests.AuthorizedSession(
+            credentials=mock.Mock()
+        )
+        with pytest.raises(exceptions.MutualTLSChannelError):
+            with mock.patch.dict(
+                os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}
+            ):
+                auth_session.configure_mtls_channel()
+
+        mock_get_client_cert_and_key.return_value = (False, None, None)
+        with mock.patch.dict("sys.modules"):
+            sys.modules["OpenSSL"] = None
+            with pytest.raises(exceptions.MutualTLSChannelError):
+                with mock.patch.dict(
+                    os.environ,
+                    {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"},
+                ):
+                    auth_session.configure_mtls_channel()
+
+    @mock.patch(
+        "google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True
+    )
+    def test_configure_mtls_channel_without_client_cert_env(
+        self, get_client_cert_and_key
+    ):
+        # Test client cert won't be used if GOOGLE_API_USE_CLIENT_CERTIFICATE
+        # environment variable is not set.
+        auth_session = google.auth.transport.requests.AuthorizedSession(
+            credentials=mock.Mock()
+        )
+
+        auth_session.configure_mtls_channel()
+        assert not auth_session.is_mtls
+        get_client_cert_and_key.assert_not_called()
+
+        mock_callback = mock.Mock()
+        auth_session.configure_mtls_channel(mock_callback)
+        assert not auth_session.is_mtls
+        mock_callback.assert_not_called()
+
+    def test_close_wo_passed_in_auth_request(self):
+        authed_session = google.auth.transport.requests.AuthorizedSession(
+            mock.sentinel.credentials
+        )
+        authed_session._auth_request_session = mock.Mock(spec=["close"])
+
+        authed_session.close()
+
+        authed_session._auth_request_session.close.assert_called_once_with()
+
+    def test_close_w_passed_in_auth_request(self):
+        http = mock.create_autospec(requests.Session)
+        auth_request = google.auth.transport.requests.Request(http)
+        authed_session = google.auth.transport.requests.AuthorizedSession(
+            mock.sentinel.credentials, auth_request=auth_request
+        )
+
+        authed_session.close()  # no raise
diff --git a/tests/transport/test_urllib3.py b/tests/transport/test_urllib3.py
new file mode 100644
index 0000000..e3848c1
--- /dev/null
+++ b/tests/transport/test_urllib3.py
@@ -0,0 +1,307 @@
+# Copyright 2016 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 os
+import sys
+
+import mock
+import OpenSSL
+import pytest
+from six.moves import http_client
+import urllib3
+
+from google.auth import environment_vars
+from google.auth import exceptions
+import google.auth.credentials
+import google.auth.transport._mtls_helper
+import google.auth.transport.urllib3
+from google.oauth2 import service_account
+from tests.transport import compliance
+
+
+class TestRequestResponse(compliance.RequestResponseTests):
+    def make_request(self):
+        http = urllib3.PoolManager()
+        return google.auth.transport.urllib3.Request(http)
+
+    def test_timeout(self):
+        http = mock.create_autospec(urllib3.PoolManager)
+        request = google.auth.transport.urllib3.Request(http)
+        request(url="http://example.com", method="GET", timeout=5)
+
+        assert http.request.call_args[1]["timeout"] == 5
+
+
+def test__make_default_http_with_certifi():
+    http = google.auth.transport.urllib3._make_default_http()
+    assert "cert_reqs" in http.connection_pool_kw
+
+
[email protected](google.auth.transport.urllib3, "certifi", new=None)
+def test__make_default_http_without_certifi():
+    http = google.auth.transport.urllib3._make_default_http()
+    assert "cert_reqs" not in http.connection_pool_kw
+
+
+class CredentialsStub(google.auth.credentials.Credentials):
+    def __init__(self, token="token"):
+        super(CredentialsStub, self).__init__()
+        self.token = token
+
+    def apply(self, headers, token=None):
+        headers["authorization"] = self.token
+
+    def before_request(self, request, method, url, headers):
+        self.apply(headers)
+
+    def refresh(self, request):
+        self.token += "1"
+
+    def with_quota_project(self, quota_project_id):
+        raise NotImplementedError()
+
+
+class HttpStub(object):
+    def __init__(self, responses, headers=None):
+        self.responses = responses
+        self.requests = []
+        self.headers = headers or {}
+
+    def urlopen(self, method, url, body=None, headers=None, **kwargs):
+        self.requests.append((method, url, body, headers, kwargs))
+        return self.responses.pop(0)
+
+
+class ResponseStub(object):
+    def __init__(self, status=http_client.OK, data=None):
+        self.status = status
+        self.data = data
+
+
+class TestMakeMutualTlsHttp(object):
+    def test_success(self):
+        http = google.auth.transport.urllib3._make_mutual_tls_http(
+            pytest.public_cert_bytes, pytest.private_key_bytes
+        )
+        assert isinstance(http, urllib3.PoolManager)
+
+    def test_crypto_error(self):
+        with pytest.raises(OpenSSL.crypto.Error):
+            google.auth.transport.urllib3._make_mutual_tls_http(
+                b"invalid cert", b"invalid key"
+            )
+
+    @mock.patch.dict("sys.modules", {"OpenSSL.crypto": None})
+    def test_import_error(self):
+        with pytest.raises(ImportError):
+            google.auth.transport.urllib3._make_mutual_tls_http(
+                pytest.public_cert_bytes, pytest.private_key_bytes
+            )
+
+
+class TestAuthorizedHttp(object):
+    TEST_URL = "http://example.com"
+
+    def test_authed_http_defaults(self):
+        authed_http = google.auth.transport.urllib3.AuthorizedHttp(
+            mock.sentinel.credentials
+        )
+
+        assert authed_http.credentials == mock.sentinel.credentials
+        assert isinstance(authed_http.http, urllib3.PoolManager)
+
+    def test_urlopen_no_refresh(self):
+        credentials = mock.Mock(wraps=CredentialsStub())
+        response = ResponseStub()
+        http = HttpStub([response])
+
+        authed_http = google.auth.transport.urllib3.AuthorizedHttp(
+            credentials, http=http
+        )
+
+        result = authed_http.urlopen("GET", self.TEST_URL)
+
+        assert result == response
+        assert credentials.before_request.called
+        assert not credentials.refresh.called
+        assert http.requests == [
+            ("GET", self.TEST_URL, None, {"authorization": "token"}, {})
+        ]
+
+    def test_urlopen_refresh(self):
+        credentials = mock.Mock(wraps=CredentialsStub())
+        final_response = ResponseStub(status=http_client.OK)
+        # First request will 401, second request will succeed.
+        http = HttpStub([ResponseStub(status=http_client.UNAUTHORIZED), final_response])
+
+        authed_http = google.auth.transport.urllib3.AuthorizedHttp(
+            credentials, http=http
+        )
+
+        authed_http = authed_http.urlopen("GET", "http://example.com")
+
+        assert authed_http == final_response
+        assert credentials.before_request.call_count == 2
+        assert credentials.refresh.called
+        assert http.requests == [
+            ("GET", self.TEST_URL, None, {"authorization": "token"}, {}),
+            ("GET", self.TEST_URL, None, {"authorization": "token1"}, {}),
+        ]
+
+    def test_urlopen_no_default_host(self):
+        credentials = mock.create_autospec(service_account.Credentials)
+
+        authed_http = google.auth.transport.urllib3.AuthorizedHttp(credentials)
+
+        authed_http.credentials._create_self_signed_jwt.assert_called_once_with(None)
+
+    def test_urlopen_with_default_host(self):
+        default_host = "pubsub.googleapis.com"
+        credentials = mock.create_autospec(service_account.Credentials)
+
+        authed_http = google.auth.transport.urllib3.AuthorizedHttp(
+            credentials, default_host=default_host
+        )
+
+        authed_http.credentials._create_self_signed_jwt.assert_called_once_with(
+            "https://{}/".format(default_host)
+        )
+
+    def test_proxies(self):
+        http = mock.create_autospec(urllib3.PoolManager)
+        authed_http = google.auth.transport.urllib3.AuthorizedHttp(None, http=http)
+
+        with authed_http:
+            pass
+
+        assert http.__enter__.called
+        assert http.__exit__.called
+
+        authed_http.headers = mock.sentinel.headers
+        assert authed_http.headers == http.headers
+
+    @mock.patch("google.auth.transport.urllib3._make_mutual_tls_http", autospec=True)
+    def test_configure_mtls_channel_with_callback(self, mock_make_mutual_tls_http):
+        callback = mock.Mock()
+        callback.return_value = (pytest.public_cert_bytes, pytest.private_key_bytes)
+
+        authed_http = google.auth.transport.urllib3.AuthorizedHttp(
+            credentials=mock.Mock(), http=mock.Mock()
+        )
+
+        with pytest.warns(UserWarning):
+            with mock.patch.dict(
+                os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}
+            ):
+                is_mtls = authed_http.configure_mtls_channel(callback)
+
+        assert is_mtls
+        mock_make_mutual_tls_http.assert_called_once_with(
+            cert=pytest.public_cert_bytes, key=pytest.private_key_bytes
+        )
+
+    @mock.patch("google.auth.transport.urllib3._make_mutual_tls_http", autospec=True)
+    @mock.patch(
+        "google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True
+    )
+    def test_configure_mtls_channel_with_metadata(
+        self, mock_get_client_cert_and_key, mock_make_mutual_tls_http
+    ):
+        authed_http = google.auth.transport.urllib3.AuthorizedHttp(
+            credentials=mock.Mock()
+        )
+
+        mock_get_client_cert_and_key.return_value = (
+            True,
+            pytest.public_cert_bytes,
+            pytest.private_key_bytes,
+        )
+        with mock.patch.dict(
+            os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}
+        ):
+            is_mtls = authed_http.configure_mtls_channel()
+
+        assert is_mtls
+        mock_get_client_cert_and_key.assert_called_once()
+        mock_make_mutual_tls_http.assert_called_once_with(
+            cert=pytest.public_cert_bytes, key=pytest.private_key_bytes
+        )
+
+    @mock.patch("google.auth.transport.urllib3._make_mutual_tls_http", autospec=True)
+    @mock.patch(
+        "google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True
+    )
+    def test_configure_mtls_channel_non_mtls(
+        self, mock_get_client_cert_and_key, mock_make_mutual_tls_http
+    ):
+        authed_http = google.auth.transport.urllib3.AuthorizedHttp(
+            credentials=mock.Mock()
+        )
+
+        mock_get_client_cert_and_key.return_value = (False, None, None)
+        with mock.patch.dict(
+            os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}
+        ):
+            is_mtls = authed_http.configure_mtls_channel()
+
+        assert not is_mtls
+        mock_get_client_cert_and_key.assert_called_once()
+        mock_make_mutual_tls_http.assert_not_called()
+
+    @mock.patch(
+        "google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True
+    )
+    def test_configure_mtls_channel_exceptions(self, mock_get_client_cert_and_key):
+        authed_http = google.auth.transport.urllib3.AuthorizedHttp(
+            credentials=mock.Mock()
+        )
+
+        mock_get_client_cert_and_key.side_effect = exceptions.ClientCertError()
+        with pytest.raises(exceptions.MutualTLSChannelError):
+            with mock.patch.dict(
+                os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}
+            ):
+                authed_http.configure_mtls_channel()
+
+        mock_get_client_cert_and_key.return_value = (False, None, None)
+        with mock.patch.dict("sys.modules"):
+            sys.modules["OpenSSL"] = None
+            with pytest.raises(exceptions.MutualTLSChannelError):
+                with mock.patch.dict(
+                    os.environ,
+                    {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"},
+                ):
+                    authed_http.configure_mtls_channel()
+
+    @mock.patch(
+        "google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True
+    )
+    def test_configure_mtls_channel_without_client_cert_env(
+        self, get_client_cert_and_key
+    ):
+        callback = mock.Mock()
+
+        authed_http = google.auth.transport.urllib3.AuthorizedHttp(
+            credentials=mock.Mock(), http=mock.Mock()
+        )
+
+        # Test the callback is not called if GOOGLE_API_USE_CLIENT_CERTIFICATE is not set.
+        is_mtls = authed_http.configure_mtls_channel(callback)
+        assert not is_mtls
+        callback.assert_not_called()
+
+        # Test ADC client cert is not used if GOOGLE_API_USE_CLIENT_CERTIFICATE is not set.
+        is_mtls = authed_http.configure_mtls_channel(callback)
+        assert not is_mtls
+        get_client_cert_and_key.assert_not_called()
diff --git a/tests_async/__init__.py b/tests_async/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests_async/__init__.py
diff --git a/tests_async/conftest.py b/tests_async/conftest.py
new file mode 100644
index 0000000..b4e90f0
--- /dev/null
+++ b/tests_async/conftest.py
@@ -0,0 +1,51 @@
+# 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.
+
+import os
+import sys
+
+import mock
+import pytest
+
+
+def pytest_configure():
+    """Load public certificate and private key."""
+    pytest.data_dir = os.path.join(
+        os.path.abspath(os.path.join(__file__, "../..")), "tests/data"
+    )
+
+    with open(os.path.join(pytest.data_dir, "privatekey.pem"), "rb") as fh:
+        pytest.private_key_bytes = fh.read()
+
+    with open(os.path.join(pytest.data_dir, "public_cert.pem"), "rb") as fh:
+        pytest.public_cert_bytes = fh.read()
+
+
[email protected]
+def mock_non_existent_module(monkeypatch):
+    """Mocks a non-existing module in sys.modules.
+
+    Additionally mocks any non-existing modules specified in the dotted path.
+    """
+
+    def _mock_non_existent_module(path):
+        parts = path.split(".")
+        partial = []
+        for part in parts:
+            partial.append(part)
+            current_module = ".".join(partial)
+            if current_module not in sys.modules:
+                monkeypatch.setitem(sys.modules, current_module, mock.MagicMock())
+
+    return _mock_non_existent_module
diff --git a/tests_async/oauth2/test__client_async.py b/tests_async/oauth2/test__client_async.py
new file mode 100644
index 0000000..6e48c45
--- /dev/null
+++ b/tests_async/oauth2/test__client_async.py
@@ -0,0 +1,304 @@
+# 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.
+
+import datetime
+import json
+
+import mock
+import pytest
+import six
+from six.moves import http_client
+from six.moves import urllib
+
+from google.auth import _helpers
+from google.auth import _jwt_async as jwt
+from google.auth import exceptions
+from google.oauth2 import _client as sync_client
+from google.oauth2 import _client_async as _client
+from tests.oauth2 import test__client as test_client
+
+
+def make_request(response_data, status=http_client.OK):
+    response = mock.AsyncMock(spec=["transport.Response"])
+    response.status = status
+    data = json.dumps(response_data).encode("utf-8")
+    response.data = mock.AsyncMock(spec=["__call__", "read"])
+    response.data.read = mock.AsyncMock(spec=["__call__"], return_value=data)
+    response.content = mock.AsyncMock(spec=["__call__"], return_value=data)
+    request = mock.AsyncMock(spec=["transport.Request"])
+    request.return_value = response
+    return request
+
+
[email protected]
+async def test__token_endpoint_request():
+
+    request = make_request({"test": "response"})
+
+    result = await _client._token_endpoint_request(
+        request, "http://example.com", {"test": "params"}
+    )
+
+    # Check request call
+    request.assert_called_with(
+        method="POST",
+        url="http://example.com",
+        headers={"Content-Type": "application/x-www-form-urlencoded"},
+        body="test=params".encode("utf-8"),
+    )
+
+    # Check result
+    assert result == {"test": "response"}
+
+
[email protected]
+async def test__token_endpoint_request_json():
+
+    request = make_request({"test": "response"})
+    access_token = "access_token"
+
+    result = await _client._token_endpoint_request(
+        request,
+        "http://example.com",
+        {"test": "params"},
+        access_token=access_token,
+        use_json=True,
+    )
+
+    # Check request call
+    request.assert_called_with(
+        method="POST",
+        url="http://example.com",
+        headers={
+            "Content-Type": "application/json",
+            "Authorization": "Bearer access_token",
+        },
+        body=b'{"test": "params"}',
+    )
+
+    # Check result
+    assert result == {"test": "response"}
+
+
[email protected]
+async def test__token_endpoint_request_error():
+    request = make_request({}, status=http_client.BAD_REQUEST)
+
+    with pytest.raises(exceptions.RefreshError):
+        await _client._token_endpoint_request(request, "http://example.com", {})
+
+
[email protected]
+async def test__token_endpoint_request_internal_failure_error():
+    request = make_request(
+        {"error_description": "internal_failure"}, status=http_client.BAD_REQUEST
+    )
+
+    with pytest.raises(exceptions.RefreshError):
+        await _client._token_endpoint_request(
+            request, "http://example.com", {"error_description": "internal_failure"}
+        )
+
+    request = make_request(
+        {"error": "internal_failure"}, status=http_client.BAD_REQUEST
+    )
+
+    with pytest.raises(exceptions.RefreshError):
+        await _client._token_endpoint_request(
+            request, "http://example.com", {"error": "internal_failure"}
+        )
+
+
+def verify_request_params(request, params):
+    request_body = request.call_args[1]["body"].decode("utf-8")
+    request_params = urllib.parse.parse_qs(request_body)
+
+    for key, value in six.iteritems(params):
+        assert request_params[key][0] == value
+
+
[email protected]("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
[email protected]
+async def test_jwt_grant(utcnow):
+    request = make_request(
+        {"access_token": "token", "expires_in": 500, "extra": "data"}
+    )
+
+    token, expiry, extra_data = await _client.jwt_grant(
+        request, "http://example.com", "assertion_value"
+    )
+
+    # Check request call
+    verify_request_params(
+        request,
+        {"grant_type": sync_client._JWT_GRANT_TYPE, "assertion": "assertion_value"},
+    )
+
+    # Check result
+    assert token == "token"
+    assert expiry == utcnow() + datetime.timedelta(seconds=500)
+    assert extra_data["extra"] == "data"
+
+
[email protected]
+async def test_jwt_grant_no_access_token():
+    request = make_request(
+        {
+            # No access token.
+            "expires_in": 500,
+            "extra": "data",
+        }
+    )
+
+    with pytest.raises(exceptions.RefreshError):
+        await _client.jwt_grant(request, "http://example.com", "assertion_value")
+
+
[email protected]
+async def test_id_token_jwt_grant():
+    now = _helpers.utcnow()
+    id_token_expiry = _helpers.datetime_to_secs(now)
+    id_token = jwt.encode(test_client.SIGNER, {"exp": id_token_expiry}).decode("utf-8")
+    request = make_request({"id_token": id_token, "extra": "data"})
+
+    token, expiry, extra_data = await _client.id_token_jwt_grant(
+        request, "http://example.com", "assertion_value"
+    )
+
+    # Check request call
+    verify_request_params(
+        request,
+        {"grant_type": sync_client._JWT_GRANT_TYPE, "assertion": "assertion_value"},
+    )
+
+    # Check result
+    assert token == id_token
+    # JWT does not store microseconds
+    now = now.replace(microsecond=0)
+    assert expiry == now
+    assert extra_data["extra"] == "data"
+
+
[email protected]
+async def test_id_token_jwt_grant_no_access_token():
+    request = make_request(
+        {
+            # No access token.
+            "expires_in": 500,
+            "extra": "data",
+        }
+    )
+
+    with pytest.raises(exceptions.RefreshError):
+        await _client.id_token_jwt_grant(
+            request, "http://example.com", "assertion_value"
+        )
+
+
[email protected]("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
[email protected]
+async def test_refresh_grant(unused_utcnow):
+    request = make_request(
+        {
+            "access_token": "token",
+            "refresh_token": "new_refresh_token",
+            "expires_in": 500,
+            "extra": "data",
+        }
+    )
+
+    token, refresh_token, expiry, extra_data = await _client.refresh_grant(
+        request,
+        "http://example.com",
+        "refresh_token",
+        "client_id",
+        "client_secret",
+        rapt_token="rapt_token",
+    )
+
+    # Check request call
+    verify_request_params(
+        request,
+        {
+            "grant_type": sync_client._REFRESH_GRANT_TYPE,
+            "refresh_token": "refresh_token",
+            "client_id": "client_id",
+            "client_secret": "client_secret",
+            "rapt": "rapt_token",
+        },
+    )
+
+    # Check result
+    assert token == "token"
+    assert refresh_token == "new_refresh_token"
+    assert expiry == datetime.datetime.min + datetime.timedelta(seconds=500)
+    assert extra_data["extra"] == "data"
+
+
[email protected]("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
[email protected]
+async def test_refresh_grant_with_scopes(unused_utcnow):
+    request = make_request(
+        {
+            "access_token": "token",
+            "refresh_token": "new_refresh_token",
+            "expires_in": 500,
+            "extra": "data",
+            "scope": test_client.SCOPES_AS_STRING,
+        }
+    )
+
+    token, refresh_token, expiry, extra_data = await _client.refresh_grant(
+        request,
+        "http://example.com",
+        "refresh_token",
+        "client_id",
+        "client_secret",
+        test_client.SCOPES_AS_LIST,
+    )
+
+    # Check request call.
+    verify_request_params(
+        request,
+        {
+            "grant_type": sync_client._REFRESH_GRANT_TYPE,
+            "refresh_token": "refresh_token",
+            "client_id": "client_id",
+            "client_secret": "client_secret",
+            "scope": test_client.SCOPES_AS_STRING,
+        },
+    )
+
+    # Check result.
+    assert token == "token"
+    assert refresh_token == "new_refresh_token"
+    assert expiry == datetime.datetime.min + datetime.timedelta(seconds=500)
+    assert extra_data["extra"] == "data"
+
+
[email protected]
+async def test_refresh_grant_no_access_token():
+    request = make_request(
+        {
+            # No access token.
+            "refresh_token": "new_refresh_token",
+            "expires_in": 500,
+            "extra": "data",
+        }
+    )
+
+    with pytest.raises(exceptions.RefreshError):
+        await _client.refresh_grant(
+            request, "http://example.com", "refresh_token", "client_id", "client_secret"
+        )
diff --git a/tests_async/oauth2/test_credentials_async.py b/tests_async/oauth2/test_credentials_async.py
new file mode 100644
index 0000000..06c9141
--- /dev/null
+++ b/tests_async/oauth2/test_credentials_async.py
@@ -0,0 +1,501 @@
+# 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.
+
+import datetime
+import json
+import os
+import pickle
+import sys
+
+import mock
+import pytest
+
+from google.auth import _helpers
+from google.auth import exceptions
+from google.oauth2 import _credentials_async as _credentials_async
+from google.oauth2 import credentials
+from tests.oauth2 import test_credentials
+
+
+class TestCredentials:
+
+    TOKEN_URI = "https://example.com/oauth2/token"
+    REFRESH_TOKEN = "refresh_token"
+    CLIENT_ID = "client_id"
+    CLIENT_SECRET = "client_secret"
+
+    @classmethod
+    def make_credentials(cls):
+        return _credentials_async.Credentials(
+            token=None,
+            refresh_token=cls.REFRESH_TOKEN,
+            token_uri=cls.TOKEN_URI,
+            client_id=cls.CLIENT_ID,
+            client_secret=cls.CLIENT_SECRET,
+            enable_reauth_refresh=True,
+        )
+
+    def test_default_state(self):
+        credentials = self.make_credentials()
+        assert not credentials.valid
+        # Expiration hasn't been set yet
+        assert not credentials.expired
+        # Scopes aren't required for these credentials
+        assert not credentials.requires_scopes
+        # Test properties
+        assert credentials.refresh_token == self.REFRESH_TOKEN
+        assert credentials.token_uri == self.TOKEN_URI
+        assert credentials.client_id == self.CLIENT_ID
+        assert credentials.client_secret == self.CLIENT_SECRET
+
+    @mock.patch("google.oauth2._reauth_async.refresh_grant", autospec=True)
+    @mock.patch(
+        "google.auth._helpers.utcnow",
+        return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD,
+    )
+    @pytest.mark.asyncio
+    async def test_refresh_success(self, unused_utcnow, refresh_grant):
+        token = "token"
+        expiry = _helpers.utcnow() + datetime.timedelta(seconds=500)
+        grant_response = {"id_token": mock.sentinel.id_token}
+        rapt_token = "rapt_token"
+        refresh_grant.return_value = (
+            # Access token
+            token,
+            # New refresh token
+            None,
+            # Expiry,
+            expiry,
+            # Extra data
+            grant_response,
+            # Rapt token
+            rapt_token,
+        )
+
+        request = mock.AsyncMock(spec=["transport.Request"])
+        creds = self.make_credentials()
+
+        # Refresh credentials
+        await creds.refresh(request)
+
+        # Check jwt grant call.
+        refresh_grant.assert_called_with(
+            request,
+            self.TOKEN_URI,
+            self.REFRESH_TOKEN,
+            self.CLIENT_ID,
+            self.CLIENT_SECRET,
+            None,
+            None,
+            True,
+        )
+
+        # Check that the credentials have the token and expiry
+        assert creds.token == token
+        assert creds.expiry == expiry
+        assert creds.id_token == mock.sentinel.id_token
+        assert creds.rapt_token == rapt_token
+
+        # Check that the credentials are valid (have a token and are not
+        # expired)
+        assert creds.valid
+
+    @pytest.mark.asyncio
+    async def test_refresh_no_refresh_token(self):
+        request = mock.AsyncMock(spec=["transport.Request"])
+        credentials_ = _credentials_async.Credentials(token=None, refresh_token=None)
+
+        with pytest.raises(exceptions.RefreshError, match="necessary fields"):
+            await credentials_.refresh(request)
+
+        request.assert_not_called()
+
+    @mock.patch("google.oauth2._reauth_async.refresh_grant", autospec=True)
+    @mock.patch(
+        "google.auth._helpers.utcnow",
+        return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD,
+    )
+    @pytest.mark.asyncio
+    async def test_credentials_with_scopes_requested_refresh_success(
+        self, unused_utcnow, refresh_grant
+    ):
+        scopes = ["email", "profile"]
+        token = "token"
+        expiry = _helpers.utcnow() + datetime.timedelta(seconds=500)
+        grant_response = {"id_token": mock.sentinel.id_token}
+        rapt_token = "rapt_token"
+        refresh_grant.return_value = (
+            # Access token
+            token,
+            # New refresh token
+            None,
+            # Expiry,
+            expiry,
+            # Extra data
+            grant_response,
+            # Rapt token
+            rapt_token,
+        )
+
+        request = mock.AsyncMock(spec=["transport.Request"])
+        creds = _credentials_async.Credentials(
+            token=None,
+            refresh_token=self.REFRESH_TOKEN,
+            token_uri=self.TOKEN_URI,
+            client_id=self.CLIENT_ID,
+            client_secret=self.CLIENT_SECRET,
+            scopes=scopes,
+            rapt_token="old_rapt_token",
+        )
+
+        # Refresh credentials
+        await creds.refresh(request)
+
+        # Check jwt grant call.
+        refresh_grant.assert_called_with(
+            request,
+            self.TOKEN_URI,
+            self.REFRESH_TOKEN,
+            self.CLIENT_ID,
+            self.CLIENT_SECRET,
+            scopes,
+            "old_rapt_token",
+            False,
+        )
+
+        # Check that the credentials have the token and expiry
+        assert creds.token == token
+        assert creds.expiry == expiry
+        assert creds.id_token == mock.sentinel.id_token
+        assert creds.has_scopes(scopes)
+        assert creds.rapt_token == rapt_token
+
+        # Check that the credentials are valid (have a token and are not
+        # expired.)
+        assert creds.valid
+
+    @mock.patch("google.oauth2._reauth_async.refresh_grant", autospec=True)
+    @mock.patch(
+        "google.auth._helpers.utcnow",
+        return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD,
+    )
+    @pytest.mark.asyncio
+    async def test_credentials_with_scopes_returned_refresh_success(
+        self, unused_utcnow, refresh_grant
+    ):
+        scopes = ["email", "profile"]
+        token = "token"
+        expiry = _helpers.utcnow() + datetime.timedelta(seconds=500)
+        grant_response = {"id_token": mock.sentinel.id_token, "scope": " ".join(scopes)}
+        rapt_token = "rapt_token"
+        refresh_grant.return_value = (
+            # Access token
+            token,
+            # New refresh token
+            None,
+            # Expiry,
+            expiry,
+            # Extra data
+            grant_response,
+            # Rapt token
+            rapt_token,
+        )
+
+        request = mock.AsyncMock(spec=["transport.Request"])
+        creds = _credentials_async.Credentials(
+            token=None,
+            refresh_token=self.REFRESH_TOKEN,
+            token_uri=self.TOKEN_URI,
+            client_id=self.CLIENT_ID,
+            client_secret=self.CLIENT_SECRET,
+            scopes=scopes,
+        )
+
+        # Refresh credentials
+        await creds.refresh(request)
+
+        # Check jwt grant call.
+        refresh_grant.assert_called_with(
+            request,
+            self.TOKEN_URI,
+            self.REFRESH_TOKEN,
+            self.CLIENT_ID,
+            self.CLIENT_SECRET,
+            scopes,
+            None,
+            False,
+        )
+
+        # Check that the credentials have the token and expiry
+        assert creds.token == token
+        assert creds.expiry == expiry
+        assert creds.id_token == mock.sentinel.id_token
+        assert creds.has_scopes(scopes)
+        assert creds.rapt_token == rapt_token
+
+        # Check that the credentials are valid (have a token and are not
+        # expired.)
+        assert creds.valid
+
+    @mock.patch("google.oauth2._reauth_async.refresh_grant", autospec=True)
+    @mock.patch(
+        "google.auth._helpers.utcnow",
+        return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD,
+    )
+    @pytest.mark.asyncio
+    async def test_credentials_with_scopes_refresh_failure_raises_refresh_error(
+        self, unused_utcnow, refresh_grant
+    ):
+        scopes = ["email", "profile"]
+        scopes_returned = ["email"]
+        token = "token"
+        expiry = _helpers.utcnow() + datetime.timedelta(seconds=500)
+        grant_response = {
+            "id_token": mock.sentinel.id_token,
+            "scope": " ".join(scopes_returned),
+        }
+        rapt_token = "rapt_token"
+        refresh_grant.return_value = (
+            # Access token
+            token,
+            # New refresh token
+            None,
+            # Expiry,
+            expiry,
+            # Extra data
+            grant_response,
+            # Rapt token
+            rapt_token,
+        )
+
+        request = mock.AsyncMock(spec=["transport.Request"])
+        creds = _credentials_async.Credentials(
+            token=None,
+            refresh_token=self.REFRESH_TOKEN,
+            token_uri=self.TOKEN_URI,
+            client_id=self.CLIENT_ID,
+            client_secret=self.CLIENT_SECRET,
+            scopes=scopes,
+            rapt_token=None,
+        )
+
+        # Refresh credentials
+        with pytest.raises(
+            exceptions.RefreshError, match="Not all requested scopes were granted"
+        ):
+            await creds.refresh(request)
+
+        # Check jwt grant call.
+        refresh_grant.assert_called_with(
+            request,
+            self.TOKEN_URI,
+            self.REFRESH_TOKEN,
+            self.CLIENT_ID,
+            self.CLIENT_SECRET,
+            scopes,
+            None,
+            False,
+        )
+
+        # Check that the credentials have the token and expiry
+        assert creds.token == token
+        assert creds.expiry == expiry
+        assert creds.id_token == mock.sentinel.id_token
+        assert creds.has_scopes(scopes)
+
+        # Check that the credentials are valid (have a token and are not
+        # expired.)
+        assert creds.valid
+
+    def test_apply_with_quota_project_id(self):
+        creds = _credentials_async.Credentials(
+            token="token",
+            refresh_token=self.REFRESH_TOKEN,
+            token_uri=self.TOKEN_URI,
+            client_id=self.CLIENT_ID,
+            client_secret=self.CLIENT_SECRET,
+            quota_project_id="quota-project-123",
+        )
+
+        headers = {}
+        creds.apply(headers)
+        assert headers["x-goog-user-project"] == "quota-project-123"
+
+    def test_apply_with_no_quota_project_id(self):
+        creds = _credentials_async.Credentials(
+            token="token",
+            refresh_token=self.REFRESH_TOKEN,
+            token_uri=self.TOKEN_URI,
+            client_id=self.CLIENT_ID,
+            client_secret=self.CLIENT_SECRET,
+        )
+
+        headers = {}
+        creds.apply(headers)
+        assert "x-goog-user-project" not in headers
+
+    def test_with_quota_project(self):
+        creds = _credentials_async.Credentials(
+            token="token",
+            refresh_token=self.REFRESH_TOKEN,
+            token_uri=self.TOKEN_URI,
+            client_id=self.CLIENT_ID,
+            client_secret=self.CLIENT_SECRET,
+            quota_project_id="quota-project-123",
+        )
+
+        new_creds = creds.with_quota_project("new-project-456")
+        assert new_creds.quota_project_id == "new-project-456"
+        headers = {}
+        creds.apply(headers)
+        assert "x-goog-user-project" in headers
+
+    def test_from_authorized_user_info(self):
+        info = test_credentials.AUTH_USER_INFO.copy()
+
+        creds = _credentials_async.Credentials.from_authorized_user_info(info)
+        assert creds.client_secret == info["client_secret"]
+        assert creds.client_id == info["client_id"]
+        assert creds.refresh_token == info["refresh_token"]
+        assert creds.token_uri == credentials._GOOGLE_OAUTH2_TOKEN_ENDPOINT
+        assert creds.scopes is None
+
+        scopes = ["email", "profile"]
+        creds = _credentials_async.Credentials.from_authorized_user_info(info, scopes)
+        assert creds.client_secret == info["client_secret"]
+        assert creds.client_id == info["client_id"]
+        assert creds.refresh_token == info["refresh_token"]
+        assert creds.token_uri == credentials._GOOGLE_OAUTH2_TOKEN_ENDPOINT
+        assert creds.scopes == scopes
+
+    def test_from_authorized_user_file(self):
+        info = test_credentials.AUTH_USER_INFO.copy()
+
+        creds = _credentials_async.Credentials.from_authorized_user_file(
+            test_credentials.AUTH_USER_JSON_FILE
+        )
+        assert creds.client_secret == info["client_secret"]
+        assert creds.client_id == info["client_id"]
+        assert creds.refresh_token == info["refresh_token"]
+        assert creds.token_uri == credentials._GOOGLE_OAUTH2_TOKEN_ENDPOINT
+        assert creds.scopes is None
+
+        scopes = ["email", "profile"]
+        creds = _credentials_async.Credentials.from_authorized_user_file(
+            test_credentials.AUTH_USER_JSON_FILE, scopes
+        )
+        assert creds.client_secret == info["client_secret"]
+        assert creds.client_id == info["client_id"]
+        assert creds.refresh_token == info["refresh_token"]
+        assert creds.token_uri == credentials._GOOGLE_OAUTH2_TOKEN_ENDPOINT
+        assert creds.scopes == scopes
+
+    def test_to_json(self):
+        info = test_credentials.AUTH_USER_INFO.copy()
+        creds = _credentials_async.Credentials.from_authorized_user_info(info)
+
+        # Test with no `strip` arg
+        json_output = creds.to_json()
+        json_asdict = json.loads(json_output)
+        assert json_asdict.get("token") == creds.token
+        assert json_asdict.get("refresh_token") == creds.refresh_token
+        assert json_asdict.get("token_uri") == creds.token_uri
+        assert json_asdict.get("client_id") == creds.client_id
+        assert json_asdict.get("scopes") == creds.scopes
+        assert json_asdict.get("client_secret") == creds.client_secret
+
+        # Test with a `strip` arg
+        json_output = creds.to_json(strip=["client_secret"])
+        json_asdict = json.loads(json_output)
+        assert json_asdict.get("token") == creds.token
+        assert json_asdict.get("refresh_token") == creds.refresh_token
+        assert json_asdict.get("token_uri") == creds.token_uri
+        assert json_asdict.get("client_id") == creds.client_id
+        assert json_asdict.get("scopes") == creds.scopes
+        assert json_asdict.get("client_secret") is None
+
+    def test_pickle_and_unpickle(self):
+        creds = self.make_credentials()
+        unpickled = pickle.loads(pickle.dumps(creds))
+
+        # make sure attributes aren't lost during pickling
+        assert list(creds.__dict__).sort() == list(unpickled.__dict__).sort()
+
+        for attr in list(creds.__dict__):
+            assert getattr(creds, attr) == getattr(unpickled, attr)
+
+    def test_pickle_with_missing_attribute(self):
+        creds = self.make_credentials()
+
+        # remove an optional attribute before pickling
+        # this mimics a pickle created with a previous class definition with
+        # fewer attributes
+        del creds.__dict__["_quota_project_id"]
+
+        unpickled = pickle.loads(pickle.dumps(creds))
+
+        # Attribute should be initialized by `__setstate__`
+        assert unpickled.quota_project_id is None
+
+    # pickles are not compatible across versions
+    @pytest.mark.skipif(
+        sys.version_info < (3, 5),
+        reason="pickle file can only be loaded with Python >= 3.5",
+    )
+    def test_unpickle_old_credentials_pickle(self):
+        # make sure a credentials file pickled with an older
+        # library version (google-auth==1.5.1) can be unpickled
+        with open(
+            os.path.join(test_credentials.DATA_DIR, "old_oauth_credentials_py3.pickle"),
+            "rb",
+        ) as f:
+            credentials = pickle.load(f)
+            assert credentials.quota_project_id is None
+
+
+class TestUserAccessTokenCredentials(object):
+    def test_instance(self):
+        cred = _credentials_async.UserAccessTokenCredentials()
+        assert cred._account is None
+
+        cred = cred.with_account("account")
+        assert cred._account == "account"
+
+    @mock.patch("google.auth._cloud_sdk.get_auth_access_token", autospec=True)
+    def test_refresh(self, get_auth_access_token):
+        get_auth_access_token.return_value = "access_token"
+        cred = _credentials_async.UserAccessTokenCredentials()
+        cred.refresh(None)
+        assert cred.token == "access_token"
+
+    def test_with_quota_project(self):
+        cred = _credentials_async.UserAccessTokenCredentials()
+        quota_project_cred = cred.with_quota_project("project-foo")
+
+        assert quota_project_cred._quota_project_id == "project-foo"
+        assert quota_project_cred._account == cred._account
+
+    @mock.patch(
+        "google.oauth2._credentials_async.UserAccessTokenCredentials.apply",
+        autospec=True,
+    )
+    @mock.patch(
+        "google.oauth2._credentials_async.UserAccessTokenCredentials.refresh",
+        autospec=True,
+    )
+    def test_before_request(self, refresh, apply):
+        cred = _credentials_async.UserAccessTokenCredentials()
+        cred.before_request(mock.Mock(), "GET", "https://example.com", {})
+        refresh.assert_called()
+        apply.assert_called()
diff --git a/tests_async/oauth2/test_id_token.py b/tests_async/oauth2/test_id_token.py
new file mode 100644
index 0000000..2aee767
--- /dev/null
+++ b/tests_async/oauth2/test_id_token.py
@@ -0,0 +1,312 @@
+# Copyright 2020 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+
+import mock
+import pytest
+
+from google.auth import environment_vars
+from google.auth import exceptions
+import google.auth.compute_engine._metadata
+from google.oauth2 import _id_token_async as id_token
+from google.oauth2 import _service_account_async
+from google.oauth2 import id_token as sync_id_token
+from tests.oauth2 import test_id_token
+
+
+def make_request(status, data=None):
+    response = mock.AsyncMock(spec=["transport.Response"])
+    response.status = status
+
+    if data is not None:
+        response.data = mock.AsyncMock(spec=["__call__", "read"])
+        response.data.read = mock.AsyncMock(spec=["__call__"], return_value=data)
+
+    request = mock.AsyncMock(spec=["transport.Request"])
+    request.return_value = response
+    return request
+
+
[email protected]
+async def test__fetch_certs_success():
+    certs = {"1": "cert"}
+    request = make_request(200, certs)
+
+    returned_certs = await id_token._fetch_certs(request, mock.sentinel.cert_url)
+
+    request.assert_called_once_with(mock.sentinel.cert_url, method="GET")
+    assert returned_certs == certs
+
+
[email protected]
+async def test__fetch_certs_failure():
+    request = make_request(404)
+
+    with pytest.raises(exceptions.TransportError):
+        await id_token._fetch_certs(request, mock.sentinel.cert_url)
+
+    request.assert_called_once_with(mock.sentinel.cert_url, method="GET")
+
+
[email protected]("google.auth.jwt.decode", autospec=True)
[email protected]("google.oauth2._id_token_async._fetch_certs", autospec=True)
[email protected]
+async def test_verify_token(_fetch_certs, decode):
+    result = await id_token.verify_token(mock.sentinel.token, mock.sentinel.request)
+
+    assert result == decode.return_value
+    _fetch_certs.assert_called_once_with(
+        mock.sentinel.request, sync_id_token._GOOGLE_OAUTH2_CERTS_URL
+    )
+    decode.assert_called_once_with(
+        mock.sentinel.token,
+        certs=_fetch_certs.return_value,
+        audience=None,
+        clock_skew_in_seconds=0,
+    )
+
+
[email protected]("google.auth.jwt.decode", autospec=True)
[email protected]("google.oauth2._id_token_async._fetch_certs", autospec=True)
[email protected]
+async def test_verify_token_clock_skew(_fetch_certs, decode):
+    result = await id_token.verify_token(
+        mock.sentinel.token, mock.sentinel.request, clock_skew_in_seconds=10
+    )
+
+    assert result == decode.return_value
+    _fetch_certs.assert_called_once_with(
+        mock.sentinel.request, sync_id_token._GOOGLE_OAUTH2_CERTS_URL
+    )
+    decode.assert_called_once_with(
+        mock.sentinel.token,
+        certs=_fetch_certs.return_value,
+        audience=None,
+        clock_skew_in_seconds=10,
+    )
+
+
[email protected]("google.auth.jwt.decode", autospec=True)
[email protected]("google.oauth2._id_token_async._fetch_certs", autospec=True)
[email protected]
+async def test_verify_token_args(_fetch_certs, decode):
+    result = await id_token.verify_token(
+        mock.sentinel.token,
+        mock.sentinel.request,
+        audience=mock.sentinel.audience,
+        certs_url=mock.sentinel.certs_url,
+    )
+
+    assert result == decode.return_value
+    _fetch_certs.assert_called_once_with(mock.sentinel.request, mock.sentinel.certs_url)
+    decode.assert_called_once_with(
+        mock.sentinel.token,
+        certs=_fetch_certs.return_value,
+        audience=mock.sentinel.audience,
+        clock_skew_in_seconds=0,
+    )
+
+
[email protected]("google.oauth2._id_token_async.verify_token", autospec=True)
[email protected]
+async def test_verify_oauth2_token(verify_token):
+    verify_token.return_value = {"iss": "accounts.google.com"}
+    result = await id_token.verify_oauth2_token(
+        mock.sentinel.token, mock.sentinel.request, audience=mock.sentinel.audience
+    )
+
+    assert result == verify_token.return_value
+    verify_token.assert_called_once_with(
+        mock.sentinel.token,
+        mock.sentinel.request,
+        audience=mock.sentinel.audience,
+        certs_url=sync_id_token._GOOGLE_OAUTH2_CERTS_URL,
+        clock_skew_in_seconds=0,
+    )
+
+
[email protected]("google.oauth2._id_token_async.verify_token", autospec=True)
[email protected]
+async def test_verify_oauth2_token_clock_skew(verify_token):
+    verify_token.return_value = {"iss": "accounts.google.com"}
+    result = await id_token.verify_oauth2_token(
+        mock.sentinel.token,
+        mock.sentinel.request,
+        audience=mock.sentinel.audience,
+        clock_skew_in_seconds=10,
+    )
+
+    assert result == verify_token.return_value
+    verify_token.assert_called_once_with(
+        mock.sentinel.token,
+        mock.sentinel.request,
+        audience=mock.sentinel.audience,
+        certs_url=sync_id_token._GOOGLE_OAUTH2_CERTS_URL,
+        clock_skew_in_seconds=10,
+    )
+
+
[email protected]("google.oauth2._id_token_async.verify_token", autospec=True)
[email protected]
+async def test_verify_oauth2_token_invalid_iss(verify_token):
+    verify_token.return_value = {"iss": "invalid_issuer"}
+
+    with pytest.raises(exceptions.GoogleAuthError):
+        await id_token.verify_oauth2_token(
+            mock.sentinel.token, mock.sentinel.request, audience=mock.sentinel.audience
+        )
+
+
[email protected]("google.oauth2._id_token_async.verify_token", autospec=True)
[email protected]
+async def test_verify_firebase_token(verify_token):
+    result = await id_token.verify_firebase_token(
+        mock.sentinel.token, mock.sentinel.request, audience=mock.sentinel.audience
+    )
+
+    assert result == verify_token.return_value
+    verify_token.assert_called_once_with(
+        mock.sentinel.token,
+        mock.sentinel.request,
+        audience=mock.sentinel.audience,
+        certs_url=sync_id_token._GOOGLE_APIS_CERTS_URL,
+        clock_skew_in_seconds=0,
+    )
+
+
[email protected]("google.oauth2._id_token_async.verify_token", autospec=True)
[email protected]
+async def test_verify_firebase_token_clock_skew(verify_token):
+    result = await id_token.verify_firebase_token(
+        mock.sentinel.token,
+        mock.sentinel.request,
+        audience=mock.sentinel.audience,
+        clock_skew_in_seconds=10,
+    )
+
+    assert result == verify_token.return_value
+    verify_token.assert_called_once_with(
+        mock.sentinel.token,
+        mock.sentinel.request,
+        audience=mock.sentinel.audience,
+        certs_url=sync_id_token._GOOGLE_APIS_CERTS_URL,
+        clock_skew_in_seconds=10,
+    )
+
+
[email protected]
+async def test_fetch_id_token_from_metadata_server(monkeypatch):
+    monkeypatch.delenv(environment_vars.CREDENTIALS, raising=False)
+
+    def mock_init(self, request, audience, use_metadata_identity_endpoint):
+        assert use_metadata_identity_endpoint
+        self.token = "id_token"
+
+    with mock.patch("google.auth.compute_engine._metadata.ping", return_value=True):
+        with mock.patch.multiple(
+            google.auth.compute_engine.IDTokenCredentials,
+            __init__=mock_init,
+            refresh=mock.Mock(),
+        ):
+            request = mock.AsyncMock()
+            token = await id_token.fetch_id_token(
+                request, "https://pubsub.googleapis.com"
+            )
+            assert token == "id_token"
+
+
[email protected]
+async def test_fetch_id_token_from_explicit_cred_json_file(monkeypatch):
+    monkeypatch.setenv(environment_vars.CREDENTIALS, test_id_token.SERVICE_ACCOUNT_FILE)
+
+    async def mock_refresh(self, request):
+        self.token = "id_token"
+
+    with mock.patch.object(
+        _service_account_async.IDTokenCredentials, "refresh", mock_refresh
+    ):
+        request = mock.AsyncMock()
+        token = await id_token.fetch_id_token(request, "https://pubsub.googleapis.com")
+        assert token == "id_token"
+
+
[email protected]
+async def test_fetch_id_token_no_cred_exists(monkeypatch):
+    monkeypatch.delenv(environment_vars.CREDENTIALS, raising=False)
+
+    with mock.patch(
+        "google.auth.compute_engine._metadata.ping",
+        side_effect=exceptions.TransportError(),
+    ):
+        with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+            request = mock.AsyncMock()
+            await id_token.fetch_id_token(request, "https://pubsub.googleapis.com")
+        assert excinfo.match(
+            r"Neither metadata server or valid service account credentials are found."
+        )
+
+    with mock.patch("google.auth.compute_engine._metadata.ping", return_value=False):
+        with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+            request = mock.AsyncMock()
+            await id_token.fetch_id_token(request, "https://pubsub.googleapis.com")
+        assert excinfo.match(
+            r"Neither metadata server or valid service account credentials are found."
+        )
+
+
[email protected]
+async def test_fetch_id_token_invalid_cred_file(monkeypatch):
+    not_json_file = os.path.join(
+        os.path.dirname(__file__), "../../tests/data/public_cert.pem"
+    )
+    monkeypatch.setenv(environment_vars.CREDENTIALS, not_json_file)
+
+    with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+        request = mock.AsyncMock()
+        await id_token.fetch_id_token(request, "https://pubsub.googleapis.com")
+    assert excinfo.match(
+        r"GOOGLE_APPLICATION_CREDENTIALS is not valid service account credentials."
+    )
+
+
[email protected]
+async def test_fetch_id_token_invalid_cred_type(monkeypatch):
+    user_credentials_file = os.path.join(
+        os.path.dirname(__file__), "../../tests/data/authorized_user.json"
+    )
+    monkeypatch.setenv(environment_vars.CREDENTIALS, user_credentials_file)
+
+    with mock.patch("google.auth.compute_engine._metadata.ping", return_value=False):
+        with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+            request = mock.AsyncMock()
+            await id_token.fetch_id_token(request, "https://pubsub.googleapis.com")
+        assert excinfo.match(
+            r"Neither metadata server or valid service account credentials are found."
+        )
+
+
[email protected]
+async def test_fetch_id_token_invalid_cred_path(monkeypatch):
+    not_json_file = os.path.join(
+        os.path.dirname(__file__), "../../tests/data/not_exists.json"
+    )
+    monkeypatch.setenv(environment_vars.CREDENTIALS, not_json_file)
+
+    with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+        request = mock.AsyncMock()
+        await id_token.fetch_id_token(request, "https://pubsub.googleapis.com")
+    assert excinfo.match(
+        r"GOOGLE_APPLICATION_CREDENTIALS path is either not found or invalid."
+    )
diff --git a/tests_async/oauth2/test_reauth_async.py b/tests_async/oauth2/test_reauth_async.py
new file mode 100644
index 0000000..d982e13
--- /dev/null
+++ b/tests_async/oauth2/test_reauth_async.py
@@ -0,0 +1,349 @@
+# 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 copy
+
+import mock
+import pytest
+
+from google.auth import exceptions
+from google.oauth2 import _reauth_async
+from google.oauth2 import reauth
+
+
+MOCK_REQUEST = mock.AsyncMock(spec=["transport.Request"])
+CHALLENGES_RESPONSE_TEMPLATE = {
+    "status": "CHALLENGE_REQUIRED",
+    "sessionId": "123",
+    "challenges": [
+        {
+            "status": "READY",
+            "challengeId": 1,
+            "challengeType": "PASSWORD",
+            "securityKey": {},
+        }
+    ],
+}
+CHALLENGES_RESPONSE_AUTHENTICATED = {
+    "status": "AUTHENTICATED",
+    "sessionId": "123",
+    "encodedProofOfReauthToken": "new_rapt_token",
+}
+
+
+class MockChallenge(object):
+    def __init__(self, name, locally_eligible, challenge_input):
+        self.name = name
+        self.is_locally_eligible = locally_eligible
+        self.challenge_input = challenge_input
+
+    def obtain_challenge_input(self, metadata):
+        return self.challenge_input
+
+
[email protected]
+async def test__get_challenges():
+    with mock.patch(
+        "google.oauth2._client_async._token_endpoint_request"
+    ) as mock_token_endpoint_request:
+        await _reauth_async._get_challenges(MOCK_REQUEST, ["SAML"], "token")
+        mock_token_endpoint_request.assert_called_with(
+            MOCK_REQUEST,
+            reauth._REAUTH_API + ":start",
+            {"supportedChallengeTypes": ["SAML"]},
+            access_token="token",
+            use_json=True,
+        )
+
+
[email protected]
+async def test__get_challenges_with_scopes():
+    with mock.patch(
+        "google.oauth2._client_async._token_endpoint_request"
+    ) as mock_token_endpoint_request:
+        await _reauth_async._get_challenges(
+            MOCK_REQUEST, ["SAML"], "token", requested_scopes=["scope"]
+        )
+        mock_token_endpoint_request.assert_called_with(
+            MOCK_REQUEST,
+            reauth._REAUTH_API + ":start",
+            {
+                "supportedChallengeTypes": ["SAML"],
+                "oauthScopesForDomainPolicyLookup": ["scope"],
+            },
+            access_token="token",
+            use_json=True,
+        )
+
+
[email protected]
+async def test__send_challenge_result():
+    with mock.patch(
+        "google.oauth2._client_async._token_endpoint_request"
+    ) as mock_token_endpoint_request:
+        await _reauth_async._send_challenge_result(
+            MOCK_REQUEST, "123", "1", {"credential": "password"}, "token"
+        )
+        mock_token_endpoint_request.assert_called_with(
+            MOCK_REQUEST,
+            reauth._REAUTH_API + "/123:continue",
+            {
+                "sessionId": "123",
+                "challengeId": "1",
+                "action": "RESPOND",
+                "proposalResponse": {"credential": "password"},
+            },
+            access_token="token",
+            use_json=True,
+        )
+
+
[email protected]
+async def test__run_next_challenge_not_ready():
+    challenges_response = copy.deepcopy(CHALLENGES_RESPONSE_TEMPLATE)
+    challenges_response["challenges"][0]["status"] = "STATUS_UNSPECIFIED"
+    assert (
+        await _reauth_async._run_next_challenge(
+            challenges_response, MOCK_REQUEST, "token"
+        )
+        is None
+    )
+
+
[email protected]
+async def test__run_next_challenge_not_supported():
+    challenges_response = copy.deepcopy(CHALLENGES_RESPONSE_TEMPLATE)
+    challenges_response["challenges"][0]["challengeType"] = "CHALLENGE_TYPE_UNSPECIFIED"
+    with pytest.raises(exceptions.ReauthFailError) as excinfo:
+        await _reauth_async._run_next_challenge(
+            challenges_response, MOCK_REQUEST, "token"
+        )
+    assert excinfo.match(r"Unsupported challenge type CHALLENGE_TYPE_UNSPECIFIED")
+
+
[email protected]
+async def test__run_next_challenge_not_locally_eligible():
+    mock_challenge = MockChallenge("PASSWORD", False, "challenge_input")
+    with mock.patch(
+        "google.oauth2.challenges.AVAILABLE_CHALLENGES", {"PASSWORD": mock_challenge}
+    ):
+        with pytest.raises(exceptions.ReauthFailError) as excinfo:
+            await _reauth_async._run_next_challenge(
+                CHALLENGES_RESPONSE_TEMPLATE, MOCK_REQUEST, "token"
+            )
+        assert excinfo.match(r"Challenge PASSWORD is not locally eligible")
+
+
[email protected]
+async def test__run_next_challenge_no_challenge_input():
+    mock_challenge = MockChallenge("PASSWORD", True, None)
+    with mock.patch(
+        "google.oauth2.challenges.AVAILABLE_CHALLENGES", {"PASSWORD": mock_challenge}
+    ):
+        assert (
+            await _reauth_async._run_next_challenge(
+                CHALLENGES_RESPONSE_TEMPLATE, MOCK_REQUEST, "token"
+            )
+            is None
+        )
+
+
[email protected]
+async def test__run_next_challenge_success():
+    mock_challenge = MockChallenge("PASSWORD", True, {"credential": "password"})
+    with mock.patch(
+        "google.oauth2.challenges.AVAILABLE_CHALLENGES", {"PASSWORD": mock_challenge}
+    ):
+        with mock.patch(
+            "google.oauth2._reauth_async._send_challenge_result"
+        ) as mock_send_challenge_result:
+            await _reauth_async._run_next_challenge(
+                CHALLENGES_RESPONSE_TEMPLATE, MOCK_REQUEST, "token"
+            )
+            mock_send_challenge_result.assert_called_with(
+                MOCK_REQUEST, "123", 1, {"credential": "password"}, "token"
+            )
+
+
[email protected]
+async def test__obtain_rapt_authenticated():
+    with mock.patch(
+        "google.oauth2._reauth_async._get_challenges",
+        return_value=CHALLENGES_RESPONSE_AUTHENTICATED,
+    ):
+        new_rapt_token = await _reauth_async._obtain_rapt(MOCK_REQUEST, "token", None)
+        assert new_rapt_token == "new_rapt_token"
+
+
[email protected]
+async def test__obtain_rapt_authenticated_after_run_next_challenge():
+    with mock.patch(
+        "google.oauth2._reauth_async._get_challenges",
+        return_value=CHALLENGES_RESPONSE_TEMPLATE,
+    ):
+        with mock.patch(
+            "google.oauth2._reauth_async._run_next_challenge",
+            side_effect=[
+                CHALLENGES_RESPONSE_TEMPLATE,
+                CHALLENGES_RESPONSE_AUTHENTICATED,
+            ],
+        ):
+            with mock.patch("google.oauth2.reauth.is_interactive", return_value=True):
+                assert (
+                    await _reauth_async._obtain_rapt(MOCK_REQUEST, "token", None)
+                    == "new_rapt_token"
+                )
+
+
[email protected]
+async def test__obtain_rapt_unsupported_status():
+    challenges_response = copy.deepcopy(CHALLENGES_RESPONSE_TEMPLATE)
+    challenges_response["status"] = "STATUS_UNSPECIFIED"
+    with mock.patch(
+        "google.oauth2._reauth_async._get_challenges", return_value=challenges_response
+    ):
+        with pytest.raises(exceptions.ReauthFailError) as excinfo:
+            await _reauth_async._obtain_rapt(MOCK_REQUEST, "token", None)
+        assert excinfo.match(r"API error: STATUS_UNSPECIFIED")
+
+
[email protected]
+async def test__obtain_rapt_not_interactive():
+    with mock.patch(
+        "google.oauth2._reauth_async._get_challenges",
+        return_value=CHALLENGES_RESPONSE_TEMPLATE,
+    ):
+        with mock.patch("google.oauth2.reauth.is_interactive", return_value=False):
+            with pytest.raises(exceptions.ReauthFailError) as excinfo:
+                await _reauth_async._obtain_rapt(MOCK_REQUEST, "token", None)
+            assert excinfo.match(r"not in an interactive session")
+
+
[email protected]
+async def test__obtain_rapt_not_authenticated():
+    with mock.patch(
+        "google.oauth2._reauth_async._get_challenges",
+        return_value=CHALLENGES_RESPONSE_TEMPLATE,
+    ):
+        with mock.patch("google.oauth2.reauth.RUN_CHALLENGE_RETRY_LIMIT", 0):
+            with pytest.raises(exceptions.ReauthFailError) as excinfo:
+                await _reauth_async._obtain_rapt(MOCK_REQUEST, "token", None)
+            assert excinfo.match(r"Reauthentication failed")
+
+
[email protected]
+async def test_get_rapt_token():
+    with mock.patch(
+        "google.oauth2._client_async.refresh_grant",
+        return_value=("token", None, None, None),
+    ) as mock_refresh_grant:
+        with mock.patch(
+            "google.oauth2._reauth_async._obtain_rapt", return_value="new_rapt_token"
+        ) as mock_obtain_rapt:
+            assert (
+                await _reauth_async.get_rapt_token(
+                    MOCK_REQUEST,
+                    "client_id",
+                    "client_secret",
+                    "refresh_token",
+                    "token_uri",
+                )
+                == "new_rapt_token"
+            )
+            mock_refresh_grant.assert_called_with(
+                request=MOCK_REQUEST,
+                client_id="client_id",
+                client_secret="client_secret",
+                refresh_token="refresh_token",
+                token_uri="token_uri",
+                scopes=[reauth._REAUTH_SCOPE],
+            )
+            mock_obtain_rapt.assert_called_with(
+                MOCK_REQUEST, "token", requested_scopes=None
+            )
+
+
[email protected]
+async def test_refresh_grant_failed():
+    with mock.patch(
+        "google.oauth2._client_async._token_endpoint_request_no_throw"
+    ) as mock_token_request:
+        mock_token_request.return_value = (False, {"error": "Bad request"})
+        with pytest.raises(exceptions.RefreshError) as excinfo:
+            await _reauth_async.refresh_grant(
+                MOCK_REQUEST,
+                "token_uri",
+                "refresh_token",
+                "client_id",
+                "client_secret",
+                scopes=["foo", "bar"],
+                rapt_token="rapt_token",
+            )
+        assert excinfo.match(r"Bad request")
+        mock_token_request.assert_called_with(
+            MOCK_REQUEST,
+            "token_uri",
+            {
+                "grant_type": "refresh_token",
+                "client_id": "client_id",
+                "client_secret": "client_secret",
+                "refresh_token": "refresh_token",
+                "scope": "foo bar",
+                "rapt": "rapt_token",
+            },
+        )
+
+
[email protected]
+async def test_refresh_grant_success():
+    with mock.patch(
+        "google.oauth2._client_async._token_endpoint_request_no_throw"
+    ) as mock_token_request:
+        mock_token_request.side_effect = [
+            (False, {"error": "invalid_grant", "error_subtype": "rapt_required"}),
+            (True, {"access_token": "access_token"}),
+        ]
+        with mock.patch(
+            "google.oauth2._reauth_async.get_rapt_token", return_value="new_rapt_token"
+        ):
+            assert await _reauth_async.refresh_grant(
+                MOCK_REQUEST,
+                "token_uri",
+                "refresh_token",
+                "client_id",
+                "client_secret",
+                enable_reauth_refresh=True,
+            ) == (
+                "access_token",
+                "refresh_token",
+                None,
+                {"access_token": "access_token"},
+                "new_rapt_token",
+            )
+
+
[email protected]
+async def test_refresh_grant_reauth_refresh_disabled():
+    with mock.patch(
+        "google.oauth2._client_async._token_endpoint_request_no_throw"
+    ) as mock_token_request:
+        mock_token_request.side_effect = [
+            (False, {"error": "invalid_grant", "error_subtype": "rapt_required"}),
+            (True, {"access_token": "access_token"}),
+        ]
+        with pytest.raises(exceptions.RefreshError) as excinfo:
+            assert await _reauth_async.refresh_grant(
+                MOCK_REQUEST, "token_uri", "refresh_token", "client_id", "client_secret"
+            )
+        assert excinfo.match(r"Reauthentication is needed")
diff --git a/tests_async/oauth2/test_service_account_async.py b/tests_async/oauth2/test_service_account_async.py
new file mode 100644
index 0000000..3dce13d
--- /dev/null
+++ b/tests_async/oauth2/test_service_account_async.py
@@ -0,0 +1,378 @@
+# 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.
+
+import datetime
+
+import mock
+import pytest
+
+from google.auth import _helpers
+from google.auth import crypt
+from google.auth import jwt
+from google.auth import transport
+from google.oauth2 import _service_account_async as service_account
+from tests.oauth2 import test_service_account
+
+
+class TestCredentials(object):
+    SERVICE_ACCOUNT_EMAIL = "[email protected]"
+    TOKEN_URI = "https://example.com/oauth2/token"
+
+    @classmethod
+    def make_credentials(cls):
+        return service_account.Credentials(
+            test_service_account.SIGNER, cls.SERVICE_ACCOUNT_EMAIL, cls.TOKEN_URI
+        )
+
+    def test_from_service_account_info(self):
+        credentials = service_account.Credentials.from_service_account_info(
+            test_service_account.SERVICE_ACCOUNT_INFO
+        )
+
+        assert (
+            credentials._signer.key_id
+            == test_service_account.SERVICE_ACCOUNT_INFO["private_key_id"]
+        )
+        assert (
+            credentials.service_account_email
+            == test_service_account.SERVICE_ACCOUNT_INFO["client_email"]
+        )
+        assert (
+            credentials._token_uri
+            == test_service_account.SERVICE_ACCOUNT_INFO["token_uri"]
+        )
+
+    def test_from_service_account_info_args(self):
+        info = test_service_account.SERVICE_ACCOUNT_INFO.copy()
+        scopes = ["email", "profile"]
+        subject = "subject"
+        additional_claims = {"meta": "data"}
+
+        credentials = service_account.Credentials.from_service_account_info(
+            info, scopes=scopes, subject=subject, additional_claims=additional_claims
+        )
+
+        assert credentials.service_account_email == info["client_email"]
+        assert credentials.project_id == info["project_id"]
+        assert credentials._signer.key_id == info["private_key_id"]
+        assert credentials._token_uri == info["token_uri"]
+        assert credentials._scopes == scopes
+        assert credentials._subject == subject
+        assert credentials._additional_claims == additional_claims
+
+    def test_from_service_account_file(self):
+        info = test_service_account.SERVICE_ACCOUNT_INFO.copy()
+
+        credentials = service_account.Credentials.from_service_account_file(
+            test_service_account.SERVICE_ACCOUNT_JSON_FILE
+        )
+
+        assert credentials.service_account_email == info["client_email"]
+        assert credentials.project_id == info["project_id"]
+        assert credentials._signer.key_id == info["private_key_id"]
+        assert credentials._token_uri == info["token_uri"]
+
+    def test_from_service_account_file_args(self):
+        info = test_service_account.SERVICE_ACCOUNT_INFO.copy()
+        scopes = ["email", "profile"]
+        subject = "subject"
+        additional_claims = {"meta": "data"}
+
+        credentials = service_account.Credentials.from_service_account_file(
+            test_service_account.SERVICE_ACCOUNT_JSON_FILE,
+            subject=subject,
+            scopes=scopes,
+            additional_claims=additional_claims,
+        )
+
+        assert credentials.service_account_email == info["client_email"]
+        assert credentials.project_id == info["project_id"]
+        assert credentials._signer.key_id == info["private_key_id"]
+        assert credentials._token_uri == info["token_uri"]
+        assert credentials._scopes == scopes
+        assert credentials._subject == subject
+        assert credentials._additional_claims == additional_claims
+
+    def test_default_state(self):
+        credentials = self.make_credentials()
+        assert not credentials.valid
+        # Expiration hasn't been set yet
+        assert not credentials.expired
+        # Scopes haven't been specified yet
+        assert credentials.requires_scopes
+
+    def test_sign_bytes(self):
+        credentials = self.make_credentials()
+        to_sign = b"123"
+        signature = credentials.sign_bytes(to_sign)
+        assert crypt.verify_signature(
+            to_sign, signature, test_service_account.PUBLIC_CERT_BYTES
+        )
+
+    def test_signer(self):
+        credentials = self.make_credentials()
+        assert isinstance(credentials.signer, crypt.Signer)
+
+    def test_signer_email(self):
+        credentials = self.make_credentials()
+        assert credentials.signer_email == self.SERVICE_ACCOUNT_EMAIL
+
+    def test_create_scoped(self):
+        credentials = self.make_credentials()
+        scopes = ["email", "profile"]
+        credentials = credentials.with_scopes(scopes)
+        assert credentials._scopes == scopes
+
+    def test_with_claims(self):
+        credentials = self.make_credentials()
+        new_credentials = credentials.with_claims({"meep": "moop"})
+        assert new_credentials._additional_claims == {"meep": "moop"}
+
+    def test_with_quota_project(self):
+        credentials = self.make_credentials()
+        new_credentials = credentials.with_quota_project("new-project-456")
+        assert new_credentials.quota_project_id == "new-project-456"
+        hdrs = {}
+        new_credentials.apply(hdrs, token="tok")
+        assert "x-goog-user-project" in hdrs
+
+    def test__make_authorization_grant_assertion(self):
+        credentials = self.make_credentials()
+        token = credentials._make_authorization_grant_assertion()
+        payload = jwt.decode(token, test_service_account.PUBLIC_CERT_BYTES)
+        assert payload["iss"] == self.SERVICE_ACCOUNT_EMAIL
+        assert (
+            payload["aud"]
+            == service_account.service_account._GOOGLE_OAUTH2_TOKEN_ENDPOINT
+        )
+
+    def test__make_authorization_grant_assertion_scoped(self):
+        credentials = self.make_credentials()
+        scopes = ["email", "profile"]
+        credentials = credentials.with_scopes(scopes)
+        token = credentials._make_authorization_grant_assertion()
+        payload = jwt.decode(token, test_service_account.PUBLIC_CERT_BYTES)
+        assert payload["scope"] == "email profile"
+
+    def test__make_authorization_grant_assertion_subject(self):
+        credentials = self.make_credentials()
+        subject = "[email protected]"
+        credentials = credentials.with_subject(subject)
+        token = credentials._make_authorization_grant_assertion()
+        payload = jwt.decode(token, test_service_account.PUBLIC_CERT_BYTES)
+        assert payload["sub"] == subject
+
+    @mock.patch("google.oauth2._client_async.jwt_grant", autospec=True)
+    @pytest.mark.asyncio
+    async def test_refresh_success(self, jwt_grant):
+        credentials = self.make_credentials()
+        token = "token"
+        jwt_grant.return_value = (
+            token,
+            _helpers.utcnow() + datetime.timedelta(seconds=500),
+            {},
+        )
+        request = mock.create_autospec(transport.Request, instance=True)
+
+        # Refresh credentials
+        await credentials.refresh(request)
+
+        # Check jwt grant call.
+        assert jwt_grant.called
+
+        called_request, token_uri, assertion = jwt_grant.call_args[0]
+        assert called_request == request
+        assert token_uri == credentials._token_uri
+        assert jwt.decode(assertion, test_service_account.PUBLIC_CERT_BYTES)
+        # No further assertion done on the token, as there are separate tests
+        # for checking the authorization grant assertion.
+
+        # Check that the credentials have the token.
+        assert credentials.token == token
+
+        # Check that the credentials are valid (have a token and are not
+        # expired)
+        assert credentials.valid
+
+    @mock.patch("google.oauth2._client_async.jwt_grant", autospec=True)
+    @pytest.mark.asyncio
+    async def test_before_request_refreshes(self, jwt_grant):
+        credentials = self.make_credentials()
+        token = "token"
+        jwt_grant.return_value = (
+            token,
+            _helpers.utcnow() + datetime.timedelta(seconds=500),
+            None,
+        )
+        request = mock.create_autospec(transport.Request, instance=True)
+
+        # Credentials should start as invalid
+        assert not credentials.valid
+
+        # before_request should cause a refresh
+        await credentials.before_request(request, "GET", "http://example.com?a=1#3", {})
+
+        # The refresh endpoint should've been called.
+        assert jwt_grant.called
+
+        # Credentials should now be valid.
+        assert credentials.valid
+
+
+class TestIDTokenCredentials(object):
+    SERVICE_ACCOUNT_EMAIL = "[email protected]"
+    TOKEN_URI = "https://example.com/oauth2/token"
+    TARGET_AUDIENCE = "https://example.com"
+
+    @classmethod
+    def make_credentials(cls):
+        return service_account.IDTokenCredentials(
+            test_service_account.SIGNER,
+            cls.SERVICE_ACCOUNT_EMAIL,
+            cls.TOKEN_URI,
+            cls.TARGET_AUDIENCE,
+        )
+
+    def test_from_service_account_info(self):
+        credentials = service_account.IDTokenCredentials.from_service_account_info(
+            test_service_account.SERVICE_ACCOUNT_INFO,
+            target_audience=self.TARGET_AUDIENCE,
+        )
+
+        assert (
+            credentials._signer.key_id
+            == test_service_account.SERVICE_ACCOUNT_INFO["private_key_id"]
+        )
+        assert (
+            credentials.service_account_email
+            == test_service_account.SERVICE_ACCOUNT_INFO["client_email"]
+        )
+        assert (
+            credentials._token_uri
+            == test_service_account.SERVICE_ACCOUNT_INFO["token_uri"]
+        )
+        assert credentials._target_audience == self.TARGET_AUDIENCE
+
+    def test_from_service_account_file(self):
+        info = test_service_account.SERVICE_ACCOUNT_INFO.copy()
+
+        credentials = service_account.IDTokenCredentials.from_service_account_file(
+            test_service_account.SERVICE_ACCOUNT_JSON_FILE,
+            target_audience=self.TARGET_AUDIENCE,
+        )
+
+        assert credentials.service_account_email == info["client_email"]
+        assert credentials._signer.key_id == info["private_key_id"]
+        assert credentials._token_uri == info["token_uri"]
+        assert credentials._target_audience == self.TARGET_AUDIENCE
+
+    def test_default_state(self):
+        credentials = self.make_credentials()
+        assert not credentials.valid
+        # Expiration hasn't been set yet
+        assert not credentials.expired
+
+    def test_sign_bytes(self):
+        credentials = self.make_credentials()
+        to_sign = b"123"
+        signature = credentials.sign_bytes(to_sign)
+        assert crypt.verify_signature(
+            to_sign, signature, test_service_account.PUBLIC_CERT_BYTES
+        )
+
+    def test_signer(self):
+        credentials = self.make_credentials()
+        assert isinstance(credentials.signer, crypt.Signer)
+
+    def test_signer_email(self):
+        credentials = self.make_credentials()
+        assert credentials.signer_email == self.SERVICE_ACCOUNT_EMAIL
+
+    def test_with_target_audience(self):
+        credentials = self.make_credentials()
+        new_credentials = credentials.with_target_audience("https://new.example.com")
+        assert new_credentials._target_audience == "https://new.example.com"
+
+    def test_with_quota_project(self):
+        credentials = self.make_credentials()
+        new_credentials = credentials.with_quota_project("project-foo")
+        assert new_credentials._quota_project_id == "project-foo"
+
+    def test__make_authorization_grant_assertion(self):
+        credentials = self.make_credentials()
+        token = credentials._make_authorization_grant_assertion()
+        payload = jwt.decode(token, test_service_account.PUBLIC_CERT_BYTES)
+        assert payload["iss"] == self.SERVICE_ACCOUNT_EMAIL
+        assert (
+            payload["aud"]
+            == service_account.service_account._GOOGLE_OAUTH2_TOKEN_ENDPOINT
+        )
+        assert payload["target_audience"] == self.TARGET_AUDIENCE
+
+    @mock.patch("google.oauth2._client_async.id_token_jwt_grant", autospec=True)
+    @pytest.mark.asyncio
+    async def test_refresh_success(self, id_token_jwt_grant):
+        credentials = self.make_credentials()
+        token = "token"
+        id_token_jwt_grant.return_value = (
+            token,
+            _helpers.utcnow() + datetime.timedelta(seconds=500),
+            {},
+        )
+
+        request = mock.AsyncMock(spec=["transport.Request"])
+
+        # Refresh credentials
+        await credentials.refresh(request)
+
+        # Check jwt grant call.
+        assert id_token_jwt_grant.called
+
+        called_request, token_uri, assertion = id_token_jwt_grant.call_args[0]
+        assert called_request == request
+        assert token_uri == credentials._token_uri
+        assert jwt.decode(assertion, test_service_account.PUBLIC_CERT_BYTES)
+        # No further assertion done on the token, as there are separate tests
+        # for checking the authorization grant assertion.
+
+        # Check that the credentials have the token.
+        assert credentials.token == token
+
+        # Check that the credentials are valid (have a token and are not
+        # expired)
+        assert credentials.valid
+
+    @mock.patch("google.oauth2._client_async.id_token_jwt_grant", autospec=True)
+    @pytest.mark.asyncio
+    async def test_before_request_refreshes(self, id_token_jwt_grant):
+        credentials = self.make_credentials()
+        token = "token"
+        id_token_jwt_grant.return_value = (
+            token,
+            _helpers.utcnow() + datetime.timedelta(seconds=500),
+            None,
+        )
+        request = mock.AsyncMock(spec=["transport.Request"])
+
+        # Credentials should start as invalid
+        assert not credentials.valid
+
+        # before_request should cause a refresh
+        await credentials.before_request(request, "GET", "http://example.com?a=1#3", {})
+
+        # The refresh endpoint should've been called.
+        assert id_token_jwt_grant.called
+
+        # Credentials should now be valid.
+        assert credentials.valid
diff --git a/tests_async/test__default_async.py b/tests_async/test__default_async.py
new file mode 100644
index 0000000..69a50d6
--- /dev/null
+++ b/tests_async/test__default_async.py
@@ -0,0 +1,563 @@
+# 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.
+
+import json
+import os
+
+import mock
+import pytest
+
+from google.auth import _credentials_async as credentials
+from google.auth import _default_async as _default
+from google.auth import app_engine
+from google.auth import compute_engine
+from google.auth import environment_vars
+from google.auth import exceptions
+from google.oauth2 import _service_account_async as service_account
+import google.oauth2.credentials
+from tests import test__default as test_default
+
+MOCK_CREDENTIALS = mock.Mock(spec=credentials.CredentialsWithQuotaProject)
+MOCK_CREDENTIALS.with_quota_project.return_value = MOCK_CREDENTIALS
+
+LOAD_FILE_PATCH = mock.patch(
+    "google.auth._default_async.load_credentials_from_file",
+    return_value=(MOCK_CREDENTIALS, mock.sentinel.project_id),
+    autospec=True,
+)
+
+
+def test_load_credentials_from_missing_file():
+    with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+        _default.load_credentials_from_file("")
+
+    assert excinfo.match(r"not found")
+
+
+def test_load_credentials_from_file_invalid_json(tmpdir):
+    jsonfile = tmpdir.join("invalid.json")
+    jsonfile.write("{")
+
+    with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+        _default.load_credentials_from_file(str(jsonfile))
+
+    assert excinfo.match(r"not a valid json file")
+
+
+def test_load_credentials_from_file_invalid_type(tmpdir):
+    jsonfile = tmpdir.join("invalid.json")
+    jsonfile.write(json.dumps({"type": "not-a-real-type"}))
+
+    with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+        _default.load_credentials_from_file(str(jsonfile))
+
+    assert excinfo.match(r"does not have a valid type")
+
+
+def test_load_credentials_from_file_authorized_user():
+    credentials, project_id = _default.load_credentials_from_file(
+        test_default.AUTHORIZED_USER_FILE
+    )
+    assert isinstance(credentials, google.oauth2._credentials_async.Credentials)
+    assert project_id is None
+
+
+def test_load_credentials_from_file_no_type(tmpdir):
+    # use the client_secrets.json, which is valid json but not a
+    # loadable credentials type
+    with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+        _default.load_credentials_from_file(test_default.CLIENT_SECRETS_FILE)
+
+    assert excinfo.match(r"does not have a valid type")
+    assert excinfo.match(r"Type is None")
+
+
+def test_load_credentials_from_file_authorized_user_bad_format(tmpdir):
+    filename = tmpdir.join("authorized_user_bad.json")
+    filename.write(json.dumps({"type": "authorized_user"}))
+
+    with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+        _default.load_credentials_from_file(str(filename))
+
+    assert excinfo.match(r"Failed to load authorized user")
+    assert excinfo.match(r"missing fields")
+
+
+def test_load_credentials_from_file_authorized_user_cloud_sdk():
+    with pytest.warns(UserWarning, match="Cloud SDK"):
+        credentials, project_id = _default.load_credentials_from_file(
+            test_default.AUTHORIZED_USER_CLOUD_SDK_FILE
+        )
+    assert isinstance(credentials, google.oauth2._credentials_async.Credentials)
+    assert project_id is None
+
+    # No warning if the json file has quota project id.
+    credentials, project_id = _default.load_credentials_from_file(
+        test_default.AUTHORIZED_USER_CLOUD_SDK_WITH_QUOTA_PROJECT_ID_FILE
+    )
+    assert isinstance(credentials, google.oauth2._credentials_async.Credentials)
+    assert project_id is None
+
+
+def test_load_credentials_from_file_authorized_user_cloud_sdk_with_scopes():
+    with pytest.warns(UserWarning, match="Cloud SDK"):
+        credentials, project_id = _default.load_credentials_from_file(
+            test_default.AUTHORIZED_USER_CLOUD_SDK_FILE,
+            scopes=["https://www.google.com/calendar/feeds"],
+        )
+    assert isinstance(credentials, google.oauth2._credentials_async.Credentials)
+    assert project_id is None
+    assert credentials.scopes == ["https://www.google.com/calendar/feeds"]
+
+
+def test_load_credentials_from_file_authorized_user_cloud_sdk_with_quota_project():
+    credentials, project_id = _default.load_credentials_from_file(
+        test_default.AUTHORIZED_USER_CLOUD_SDK_FILE, quota_project_id="project-foo"
+    )
+
+    assert isinstance(credentials, google.oauth2._credentials_async.Credentials)
+    assert project_id is None
+    assert credentials.quota_project_id == "project-foo"
+
+
+def test_load_credentials_from_file_service_account():
+    credentials, project_id = _default.load_credentials_from_file(
+        test_default.SERVICE_ACCOUNT_FILE
+    )
+    assert isinstance(credentials, service_account.Credentials)
+    assert project_id == test_default.SERVICE_ACCOUNT_FILE_DATA["project_id"]
+
+
+def test_load_credentials_from_file_service_account_with_scopes():
+    credentials, project_id = _default.load_credentials_from_file(
+        test_default.SERVICE_ACCOUNT_FILE,
+        scopes=["https://www.google.com/calendar/feeds"],
+    )
+    assert isinstance(credentials, service_account.Credentials)
+    assert project_id == test_default.SERVICE_ACCOUNT_FILE_DATA["project_id"]
+    assert credentials.scopes == ["https://www.google.com/calendar/feeds"]
+
+
+def test_load_credentials_from_file_service_account_bad_format(tmpdir):
+    filename = tmpdir.join("serivce_account_bad.json")
+    filename.write(json.dumps({"type": "service_account"}))
+
+    with pytest.raises(exceptions.DefaultCredentialsError) as excinfo:
+        _default.load_credentials_from_file(str(filename))
+
+    assert excinfo.match(r"Failed to load service account")
+    assert excinfo.match(r"missing fields")
+
+
[email protected](os.environ, {}, clear=True)
+def test__get_explicit_environ_credentials_no_env():
+    assert _default._get_explicit_environ_credentials() == (None, None)
+
+
[email protected]("quota_project_id", [None, "project-foo"])
+@LOAD_FILE_PATCH
+def test__get_explicit_environ_credentials(load, quota_project_id, monkeypatch):
+    monkeypatch.setenv(environment_vars.CREDENTIALS, "filename")
+
+    credentials, project_id = _default._get_explicit_environ_credentials(
+        quota_project_id=quota_project_id
+    )
+
+    assert credentials is MOCK_CREDENTIALS
+    assert project_id is mock.sentinel.project_id
+    load.assert_called_with("filename", quota_project_id=quota_project_id)
+
+
+@LOAD_FILE_PATCH
+def test__get_explicit_environ_credentials_no_project_id(load, monkeypatch):
+    load.return_value = MOCK_CREDENTIALS, None
+    monkeypatch.setenv(environment_vars.CREDENTIALS, "filename")
+
+    credentials, project_id = _default._get_explicit_environ_credentials()
+
+    assert credentials is MOCK_CREDENTIALS
+    assert project_id is None
+
+
[email protected]("quota_project_id", [None, "project-foo"])
[email protected](
+    "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True
+)
[email protected]("google.auth._default_async._get_gcloud_sdk_credentials", autospec=True)
+def test__get_explicit_environ_credentials_fallback_to_gcloud(
+    get_gcloud_creds, get_adc_path, quota_project_id, monkeypatch
+):
+    # Set explicit credentials path to cloud sdk credentials path.
+    get_adc_path.return_value = "filename"
+    monkeypatch.setenv(environment_vars.CREDENTIALS, "filename")
+
+    _default._get_explicit_environ_credentials(quota_project_id=quota_project_id)
+
+    # Check we fall back to cloud sdk flow since explicit credentials path is
+    # cloud sdk credentials path
+    get_gcloud_creds.assert_called_with(quota_project_id=quota_project_id)
+
+
[email protected]("quota_project_id", [None, "project-foo"])
+@LOAD_FILE_PATCH
[email protected](
+    "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True
+)
+def test__get_gcloud_sdk_credentials(get_adc_path, load, quota_project_id):
+    get_adc_path.return_value = test_default.SERVICE_ACCOUNT_FILE
+
+    credentials, project_id = _default._get_gcloud_sdk_credentials(
+        quota_project_id=quota_project_id
+    )
+
+    assert credentials is MOCK_CREDENTIALS
+    assert project_id is mock.sentinel.project_id
+    load.assert_called_with(
+        test_default.SERVICE_ACCOUNT_FILE, quota_project_id=quota_project_id
+    )
+
+
[email protected](
+    "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True
+)
+def test__get_gcloud_sdk_credentials_non_existent(get_adc_path, tmpdir):
+    non_existent = tmpdir.join("non-existent")
+    get_adc_path.return_value = str(non_existent)
+
+    credentials, project_id = _default._get_gcloud_sdk_credentials()
+
+    assert credentials is None
+    assert project_id is None
+
+
[email protected](
+    "google.auth._cloud_sdk.get_project_id",
+    return_value=mock.sentinel.project_id,
+    autospec=True,
+)
[email protected]("os.path.isfile", return_value=True, autospec=True)
+@LOAD_FILE_PATCH
+def test__get_gcloud_sdk_credentials_project_id(load, unused_isfile, get_project_id):
+    # Don't return a project ID from load file, make the function check
+    # the Cloud SDK project.
+    load.return_value = MOCK_CREDENTIALS, None
+
+    credentials, project_id = _default._get_gcloud_sdk_credentials()
+
+    assert credentials == MOCK_CREDENTIALS
+    assert project_id == mock.sentinel.project_id
+    assert get_project_id.called
+
+
[email protected]("google.auth._cloud_sdk.get_project_id", return_value=None, autospec=True)
[email protected]("os.path.isfile", return_value=True)
+@LOAD_FILE_PATCH
+def test__get_gcloud_sdk_credentials_no_project_id(load, unused_isfile, get_project_id):
+    # Don't return a project ID from load file, make the function check
+    # the Cloud SDK project.
+    load.return_value = MOCK_CREDENTIALS, None
+
+    credentials, project_id = _default._get_gcloud_sdk_credentials()
+
+    assert credentials == MOCK_CREDENTIALS
+    assert project_id is None
+    assert get_project_id.called
+
+
+class _AppIdentityModule(object):
+    """The interface of the App Idenity app engine module.
+    See https://cloud.google.com/appengine/docs/standard/python/refdocs\
+    /google.appengine.api.app_identity.app_identity
+    """
+
+    def get_application_id(self):
+        raise NotImplementedError()
+
+
[email protected]
+def app_identity(monkeypatch):
+    """Mocks the app_identity module for google.auth.app_engine."""
+    app_identity_module = mock.create_autospec(_AppIdentityModule, instance=True)
+    monkeypatch.setattr(app_engine, "app_identity", app_identity_module)
+    yield app_identity_module
+
+
[email protected](os.environ)
+def test__get_gae_credentials_gen1(app_identity):
+    os.environ[environment_vars.LEGACY_APPENGINE_RUNTIME] = "python27"
+    app_identity.get_application_id.return_value = mock.sentinel.project
+
+    credentials, project_id = _default._get_gae_credentials()
+
+    assert isinstance(credentials, app_engine.Credentials)
+    assert project_id == mock.sentinel.project
+
+
[email protected](os.environ)
+def test__get_gae_credentials_gen2():
+    os.environ["GAE_RUNTIME"] = "python37"
+    credentials, project_id = _default._get_gae_credentials()
+    assert credentials is None
+    assert project_id is None
+
+
[email protected](os.environ)
+def test__get_gae_credentials_gen2_backwards_compat():
+    # compat helpers may copy GAE_RUNTIME to APPENGINE_RUNTIME
+    # for backwards compatibility with code that relies on it
+    os.environ[environment_vars.LEGACY_APPENGINE_RUNTIME] = "python37"
+    os.environ["GAE_RUNTIME"] = "python37"
+    credentials, project_id = _default._get_gae_credentials()
+    assert credentials is None
+    assert project_id is None
+
+
+def test__get_gae_credentials_env_unset():
+    assert environment_vars.LEGACY_APPENGINE_RUNTIME not in os.environ
+    assert "GAE_RUNTIME" not in os.environ
+    credentials, project_id = _default._get_gae_credentials()
+    assert credentials is None
+    assert project_id is None
+
+
[email protected](os.environ)
+def test__get_gae_credentials_no_app_engine():
+    # test both with and without LEGACY_APPENGINE_RUNTIME setting
+    assert environment_vars.LEGACY_APPENGINE_RUNTIME not in os.environ
+
+    import sys
+
+    with mock.patch.dict(sys.modules, {"google.auth.app_engine": None}):
+        credentials, project_id = _default._get_gae_credentials()
+        assert credentials is None
+        assert project_id is None
+
+        os.environ[environment_vars.LEGACY_APPENGINE_RUNTIME] = "python27"
+        credentials, project_id = _default._get_gae_credentials()
+        assert credentials is None
+        assert project_id is None
+
+
[email protected](os.environ)
[email protected](app_engine, "app_identity", new=None)
+def test__get_gae_credentials_no_apis():
+    # test both with and without LEGACY_APPENGINE_RUNTIME setting
+    assert environment_vars.LEGACY_APPENGINE_RUNTIME not in os.environ
+
+    credentials, project_id = _default._get_gae_credentials()
+    assert credentials is None
+    assert project_id is None
+
+    os.environ[environment_vars.LEGACY_APPENGINE_RUNTIME] = "python27"
+    credentials, project_id = _default._get_gae_credentials()
+    assert credentials is None
+    assert project_id is None
+
+
[email protected](
+    "google.auth.compute_engine._metadata.ping", return_value=True, autospec=True
+)
[email protected](
+    "google.auth.compute_engine._metadata.get_project_id",
+    return_value="example-project",
+    autospec=True,
+)
+def test__get_gce_credentials(unused_get, unused_ping):
+    credentials, project_id = _default._get_gce_credentials()
+
+    assert isinstance(credentials, compute_engine.Credentials)
+    assert project_id == "example-project"
+
+
[email protected](
+    "google.auth.compute_engine._metadata.ping", return_value=False, autospec=True
+)
+def test__get_gce_credentials_no_ping(unused_ping):
+    credentials, project_id = _default._get_gce_credentials()
+
+    assert credentials is None
+    assert project_id is None
+
+
[email protected](
+    "google.auth.compute_engine._metadata.ping", return_value=True, autospec=True
+)
[email protected](
+    "google.auth.compute_engine._metadata.get_project_id",
+    side_effect=exceptions.TransportError(),
+    autospec=True,
+)
+def test__get_gce_credentials_no_project_id(unused_get, unused_ping):
+    credentials, project_id = _default._get_gce_credentials()
+
+    assert isinstance(credentials, compute_engine.Credentials)
+    assert project_id is None
+
+
+def test__get_gce_credentials_no_compute_engine():
+    import sys
+
+    with mock.patch.dict("sys.modules"):
+        sys.modules["google.auth.compute_engine"] = None
+        credentials, project_id = _default._get_gce_credentials()
+        assert credentials is None
+        assert project_id is None
+
+
[email protected](
+    "google.auth.compute_engine._metadata.ping", return_value=False, autospec=True
+)
+def test__get_gce_credentials_explicit_request(ping):
+    _default._get_gce_credentials(mock.sentinel.request)
+    ping.assert_called_with(request=mock.sentinel.request)
+
+
[email protected](
+    "google.auth._default_async._get_explicit_environ_credentials",
+    return_value=(MOCK_CREDENTIALS, mock.sentinel.project_id),
+    autospec=True,
+)
+def test_default_early_out(unused_get):
+    assert _default.default_async() == (MOCK_CREDENTIALS, mock.sentinel.project_id)
+
+
[email protected](
+    "google.auth._default_async._get_explicit_environ_credentials",
+    return_value=(MOCK_CREDENTIALS, mock.sentinel.project_id),
+    autospec=True,
+)
+def test_default_explict_project_id(unused_get, monkeypatch):
+    monkeypatch.setenv(environment_vars.PROJECT, "explicit-env")
+    assert _default.default_async() == (MOCK_CREDENTIALS, "explicit-env")
+
+
[email protected](
+    "google.auth._default_async._get_explicit_environ_credentials",
+    return_value=(MOCK_CREDENTIALS, mock.sentinel.project_id),
+    autospec=True,
+)
+def test_default_explict_legacy_project_id(unused_get, monkeypatch):
+    monkeypatch.setenv(environment_vars.LEGACY_PROJECT, "explicit-env")
+    assert _default.default_async() == (MOCK_CREDENTIALS, "explicit-env")
+
+
[email protected]("logging.Logger.warning", autospec=True)
[email protected](
+    "google.auth._default_async._get_explicit_environ_credentials",
+    return_value=(MOCK_CREDENTIALS, None),
+    autospec=True,
+)
[email protected](
+    "google.auth._default_async._get_gcloud_sdk_credentials",
+    return_value=(MOCK_CREDENTIALS, None),
+    autospec=True,
+)
[email protected](
+    "google.auth._default_async._get_gae_credentials",
+    return_value=(MOCK_CREDENTIALS, None),
+    autospec=True,
+)
[email protected](
+    "google.auth._default_async._get_gce_credentials",
+    return_value=(MOCK_CREDENTIALS, None),
+    autospec=True,
+)
+def test_default_without_project_id(
+    unused_gce, unused_gae, unused_sdk, unused_explicit, logger_warning
+):
+    assert _default.default_async() == (MOCK_CREDENTIALS, None)
+    logger_warning.assert_called_with(mock.ANY, mock.ANY, mock.ANY)
+
+
[email protected](
+    "google.auth._default_async._get_explicit_environ_credentials",
+    return_value=(None, None),
+    autospec=True,
+)
[email protected](
+    "google.auth._default_async._get_gcloud_sdk_credentials",
+    return_value=(None, None),
+    autospec=True,
+)
[email protected](
+    "google.auth._default_async._get_gae_credentials",
+    return_value=(None, None),
+    autospec=True,
+)
[email protected](
+    "google.auth._default_async._get_gce_credentials",
+    return_value=(None, None),
+    autospec=True,
+)
+def test_default_fail(unused_gce, unused_gae, unused_sdk, unused_explicit):
+    with pytest.raises(exceptions.DefaultCredentialsError):
+        assert _default.default_async()
+
+
[email protected](
+    "google.auth._default_async._get_explicit_environ_credentials",
+    return_value=(MOCK_CREDENTIALS, mock.sentinel.project_id),
+    autospec=True,
+)
[email protected](
+    "google.auth._credentials_async.with_scopes_if_required",
+    return_value=MOCK_CREDENTIALS,
+    autospec=True,
+)
+def test_default_scoped(with_scopes, unused_get):
+    scopes = ["one", "two"]
+
+    credentials, project_id = _default.default_async(scopes=scopes)
+
+    assert credentials == with_scopes.return_value
+    assert project_id == mock.sentinel.project_id
+    with_scopes.assert_called_once_with(MOCK_CREDENTIALS, scopes)
+
+
[email protected](
+    "google.auth._default_async._get_explicit_environ_credentials",
+    return_value=(MOCK_CREDENTIALS, mock.sentinel.project_id),
+    autospec=True,
+)
+def test_default_no_app_engine_compute_engine_module(unused_get):
+    """
+    google.auth.compute_engine and google.auth.app_engine are both optional
+    to allow not including them when using this package. This verifies
+    that default fails gracefully if these modules are absent
+    """
+    import sys
+
+    with mock.patch.dict("sys.modules"):
+        sys.modules["google.auth.compute_engine"] = None
+        sys.modules["google.auth.app_engine"] = None
+        assert _default.default_async() == (MOCK_CREDENTIALS, mock.sentinel.project_id)
+
+
[email protected](
+    "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True
+)
+def test_default_warning_without_quota_project_id_for_user_creds(get_adc_path):
+    get_adc_path.return_value = test_default.AUTHORIZED_USER_CLOUD_SDK_FILE
+
+    with pytest.warns(UserWarning, match="Cloud SDK"):
+        credentials, project_id = _default.default_async(quota_project_id=None)
+
+
[email protected](
+    "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True
+)
+def test_default_no_warning_with_quota_project_id_for_user_creds(get_adc_path):
+    get_adc_path.return_value = test_default.AUTHORIZED_USER_CLOUD_SDK_FILE
+
+    credentials, project_id = _default.default_async(quota_project_id="project-foo")
diff --git a/tests_async/test_credentials_async.py b/tests_async/test_credentials_async.py
new file mode 100644
index 0000000..5315483
--- /dev/null
+++ b/tests_async/test_credentials_async.py
@@ -0,0 +1,179 @@
+# 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.
+
+import datetime
+
+import pytest
+
+from google.auth import _credentials_async as credentials
+from google.auth import _helpers
+
+
+class CredentialsImpl(credentials.Credentials):
+    def refresh(self, request):
+        self.token = request
+
+    def with_quota_project(self, quota_project_id):
+        raise NotImplementedError()
+
+
+def test_credentials_constructor():
+    credentials = CredentialsImpl()
+    assert not credentials.token
+    assert not credentials.expiry
+    assert not credentials.expired
+    assert not credentials.valid
+
+
+def test_expired_and_valid():
+    credentials = CredentialsImpl()
+    credentials.token = "token"
+
+    assert credentials.valid
+    assert not credentials.expired
+
+    # Set the expiration to one second more than now plus the clock skew
+    # accomodation. These credentials should be valid.
+    credentials.expiry = (
+        datetime.datetime.utcnow()
+        + _helpers.REFRESH_THRESHOLD
+        + datetime.timedelta(seconds=1)
+    )
+
+    assert credentials.valid
+    assert not credentials.expired
+
+    # Set the credentials expiration to now. Because of the clock skew
+    # accomodation, these credentials should report as expired.
+    credentials.expiry = datetime.datetime.utcnow()
+
+    assert not credentials.valid
+    assert credentials.expired
+
+
[email protected]
+async def test_before_request():
+    credentials = CredentialsImpl()
+    request = "token"
+    headers = {}
+
+    # First call should call refresh, setting the token.
+    await credentials.before_request(request, "http://example.com", "GET", headers)
+    assert credentials.valid
+    assert credentials.token == "token"
+    assert headers["authorization"] == "Bearer token"
+
+    request = "token2"
+    headers = {}
+
+    # Second call shouldn't call refresh.
+    credentials.before_request(request, "http://example.com", "GET", headers)
+
+    assert credentials.valid
+    assert credentials.token == "token"
+
+
+def test_anonymous_credentials_ctor():
+    anon = credentials.AnonymousCredentials()
+
+    assert anon.token is None
+    assert anon.expiry is None
+    assert not anon.expired
+    assert anon.valid
+
+
+def test_anonymous_credentials_refresh():
+    anon = credentials.AnonymousCredentials()
+
+    request = object()
+    with pytest.raises(ValueError):
+        anon.refresh(request)
+
+
+def test_anonymous_credentials_apply_default():
+    anon = credentials.AnonymousCredentials()
+    headers = {}
+    anon.apply(headers)
+    assert headers == {}
+    with pytest.raises(ValueError):
+        anon.apply(headers, token="TOKEN")
+
+
+def test_anonymous_credentials_before_request():
+    anon = credentials.AnonymousCredentials()
+    request = object()
+    method = "GET"
+    url = "https://example.com/api/endpoint"
+    headers = {}
+    anon.before_request(request, method, url, headers)
+    assert headers == {}
+
+
+class ReadOnlyScopedCredentialsImpl(credentials.ReadOnlyScoped, CredentialsImpl):
+    @property
+    def requires_scopes(self):
+        return super(ReadOnlyScopedCredentialsImpl, self).requires_scopes
+
+
+def test_readonly_scoped_credentials_constructor():
+    credentials = ReadOnlyScopedCredentialsImpl()
+    assert credentials._scopes is None
+
+
+def test_readonly_scoped_credentials_scopes():
+    credentials = ReadOnlyScopedCredentialsImpl()
+    credentials._scopes = ["one", "two"]
+    assert credentials.scopes == ["one", "two"]
+    assert credentials.has_scopes(["one"])
+    assert credentials.has_scopes(["two"])
+    assert credentials.has_scopes(["one", "two"])
+    assert not credentials.has_scopes(["three"])
+
+
+def test_readonly_scoped_credentials_requires_scopes():
+    credentials = ReadOnlyScopedCredentialsImpl()
+    assert not credentials.requires_scopes
+
+
+class RequiresScopedCredentialsImpl(credentials.Scoped, CredentialsImpl):
+    def __init__(self, scopes=None):
+        super(RequiresScopedCredentialsImpl, self).__init__()
+        self._scopes = scopes
+
+    @property
+    def requires_scopes(self):
+        return not self.scopes
+
+    def with_scopes(self, scopes):
+        return RequiresScopedCredentialsImpl(scopes=scopes)
+
+
+def test_create_scoped_if_required_scoped():
+    unscoped_credentials = RequiresScopedCredentialsImpl()
+    scoped_credentials = credentials.with_scopes_if_required(
+        unscoped_credentials, ["one", "two"]
+    )
+
+    assert scoped_credentials is not unscoped_credentials
+    assert not scoped_credentials.requires_scopes
+    assert scoped_credentials.has_scopes(["one", "two"])
+
+
+def test_create_scoped_if_required_not_scopes():
+    unscoped_credentials = CredentialsImpl()
+    scoped_credentials = credentials.with_scopes_if_required(
+        unscoped_credentials, ["one", "two"]
+    )
+
+    assert scoped_credentials is unscoped_credentials
diff --git a/tests_async/test_jwt_async.py b/tests_async/test_jwt_async.py
new file mode 100644
index 0000000..a35b837
--- /dev/null
+++ b/tests_async/test_jwt_async.py
@@ -0,0 +1,356 @@
+# Copyright 2020 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import datetime
+import json
+
+import mock
+import pytest
+
+from google.auth import _jwt_async as jwt_async
+from google.auth import crypt
+from google.auth import exceptions
+from tests import test_jwt
+
+
[email protected]
+def signer():
+    return crypt.RSASigner.from_string(test_jwt.PRIVATE_KEY_BYTES, "1")
+
+
+class TestCredentials(object):
+    SERVICE_ACCOUNT_EMAIL = "[email protected]"
+    SUBJECT = "subject"
+    AUDIENCE = "audience"
+    ADDITIONAL_CLAIMS = {"meta": "data"}
+    credentials = None
+
+    @pytest.fixture(autouse=True)
+    def credentials_fixture(self, signer):
+        self.credentials = jwt_async.Credentials(
+            signer,
+            self.SERVICE_ACCOUNT_EMAIL,
+            self.SERVICE_ACCOUNT_EMAIL,
+            self.AUDIENCE,
+        )
+
+    def test_from_service_account_info(self):
+        with open(test_jwt.SERVICE_ACCOUNT_JSON_FILE, "r") as fh:
+            info = json.load(fh)
+
+        credentials = jwt_async.Credentials.from_service_account_info(
+            info, audience=self.AUDIENCE
+        )
+
+        assert credentials._signer.key_id == info["private_key_id"]
+        assert credentials._issuer == info["client_email"]
+        assert credentials._subject == info["client_email"]
+        assert credentials._audience == self.AUDIENCE
+
+    def test_from_service_account_info_args(self):
+        info = test_jwt.SERVICE_ACCOUNT_INFO.copy()
+
+        credentials = jwt_async.Credentials.from_service_account_info(
+            info,
+            subject=self.SUBJECT,
+            audience=self.AUDIENCE,
+            additional_claims=self.ADDITIONAL_CLAIMS,
+        )
+
+        assert credentials._signer.key_id == info["private_key_id"]
+        assert credentials._issuer == info["client_email"]
+        assert credentials._subject == self.SUBJECT
+        assert credentials._audience == self.AUDIENCE
+        assert credentials._additional_claims == self.ADDITIONAL_CLAIMS
+
+    def test_from_service_account_file(self):
+        info = test_jwt.SERVICE_ACCOUNT_INFO.copy()
+
+        credentials = jwt_async.Credentials.from_service_account_file(
+            test_jwt.SERVICE_ACCOUNT_JSON_FILE, audience=self.AUDIENCE
+        )
+
+        assert credentials._signer.key_id == info["private_key_id"]
+        assert credentials._issuer == info["client_email"]
+        assert credentials._subject == info["client_email"]
+        assert credentials._audience == self.AUDIENCE
+
+    def test_from_service_account_file_args(self):
+        info = test_jwt.SERVICE_ACCOUNT_INFO.copy()
+
+        credentials = jwt_async.Credentials.from_service_account_file(
+            test_jwt.SERVICE_ACCOUNT_JSON_FILE,
+            subject=self.SUBJECT,
+            audience=self.AUDIENCE,
+            additional_claims=self.ADDITIONAL_CLAIMS,
+        )
+
+        assert credentials._signer.key_id == info["private_key_id"]
+        assert credentials._issuer == info["client_email"]
+        assert credentials._subject == self.SUBJECT
+        assert credentials._audience == self.AUDIENCE
+        assert credentials._additional_claims == self.ADDITIONAL_CLAIMS
+
+    def test_from_signing_credentials(self):
+        jwt_from_signing = self.credentials.from_signing_credentials(
+            self.credentials, audience=mock.sentinel.new_audience
+        )
+        jwt_from_info = jwt_async.Credentials.from_service_account_info(
+            test_jwt.SERVICE_ACCOUNT_INFO, audience=mock.sentinel.new_audience
+        )
+
+        assert isinstance(jwt_from_signing, jwt_async.Credentials)
+        assert jwt_from_signing._signer.key_id == jwt_from_info._signer.key_id
+        assert jwt_from_signing._issuer == jwt_from_info._issuer
+        assert jwt_from_signing._subject == jwt_from_info._subject
+        assert jwt_from_signing._audience == jwt_from_info._audience
+
+    def test_default_state(self):
+        assert not self.credentials.valid
+        # Expiration hasn't been set yet
+        assert not self.credentials.expired
+
+    def test_with_claims(self):
+        new_audience = "new_audience"
+        new_credentials = self.credentials.with_claims(audience=new_audience)
+
+        assert new_credentials._signer == self.credentials._signer
+        assert new_credentials._issuer == self.credentials._issuer
+        assert new_credentials._subject == self.credentials._subject
+        assert new_credentials._audience == new_audience
+        assert new_credentials._additional_claims == self.credentials._additional_claims
+        assert new_credentials._quota_project_id == self.credentials._quota_project_id
+
+    def test_with_quota_project(self):
+        quota_project_id = "project-foo"
+
+        new_credentials = self.credentials.with_quota_project(quota_project_id)
+        assert new_credentials._signer == self.credentials._signer
+        assert new_credentials._issuer == self.credentials._issuer
+        assert new_credentials._subject == self.credentials._subject
+        assert new_credentials._audience == self.credentials._audience
+        assert new_credentials._additional_claims == self.credentials._additional_claims
+        assert new_credentials._quota_project_id == quota_project_id
+
+    def test_sign_bytes(self):
+        to_sign = b"123"
+        signature = self.credentials.sign_bytes(to_sign)
+        assert crypt.verify_signature(to_sign, signature, test_jwt.PUBLIC_CERT_BYTES)
+
+    def test_signer(self):
+        assert isinstance(self.credentials.signer, crypt.RSASigner)
+
+    def test_signer_email(self):
+        assert (
+            self.credentials.signer_email
+            == test_jwt.SERVICE_ACCOUNT_INFO["client_email"]
+        )
+
+    def _verify_token(self, token):
+        payload = jwt_async.decode(token, test_jwt.PUBLIC_CERT_BYTES)
+        assert payload["iss"] == self.SERVICE_ACCOUNT_EMAIL
+        return payload
+
+    def test_refresh(self):
+        self.credentials.refresh(None)
+        assert self.credentials.valid
+        assert not self.credentials.expired
+
+    def test_expired(self):
+        assert not self.credentials.expired
+
+        self.credentials.refresh(None)
+        assert not self.credentials.expired
+
+        with mock.patch("google.auth._helpers.utcnow") as now:
+            one_day = datetime.timedelta(days=1)
+            now.return_value = self.credentials.expiry + one_day
+            assert self.credentials.expired
+
+    @pytest.mark.asyncio
+    async def test_before_request(self):
+        headers = {}
+
+        self.credentials.refresh(None)
+        await self.credentials.before_request(
+            None, "GET", "http://example.com?a=1#3", headers
+        )
+
+        header_value = headers["authorization"]
+        _, token = header_value.split(" ")
+
+        # Since the audience is set, it should use the existing token.
+        assert token.encode("utf-8") == self.credentials.token
+
+        payload = self._verify_token(token)
+        assert payload["aud"] == self.AUDIENCE
+
+    @pytest.mark.asyncio
+    async def test_before_request_refreshes(self):
+        assert not self.credentials.valid
+        await self.credentials.before_request(
+            None, "GET", "http://example.com?a=1#3", {}
+        )
+        assert self.credentials.valid
+
+
+class TestOnDemandCredentials(object):
+    SERVICE_ACCOUNT_EMAIL = "[email protected]"
+    SUBJECT = "subject"
+    ADDITIONAL_CLAIMS = {"meta": "data"}
+    credentials = None
+
+    @pytest.fixture(autouse=True)
+    def credentials_fixture(self, signer):
+        self.credentials = jwt_async.OnDemandCredentials(
+            signer,
+            self.SERVICE_ACCOUNT_EMAIL,
+            self.SERVICE_ACCOUNT_EMAIL,
+            max_cache_size=2,
+        )
+
+    def test_from_service_account_info(self):
+        with open(test_jwt.SERVICE_ACCOUNT_JSON_FILE, "r") as fh:
+            info = json.load(fh)
+
+        credentials = jwt_async.OnDemandCredentials.from_service_account_info(info)
+
+        assert credentials._signer.key_id == info["private_key_id"]
+        assert credentials._issuer == info["client_email"]
+        assert credentials._subject == info["client_email"]
+
+    def test_from_service_account_info_args(self):
+        info = test_jwt.SERVICE_ACCOUNT_INFO.copy()
+
+        credentials = jwt_async.OnDemandCredentials.from_service_account_info(
+            info, subject=self.SUBJECT, additional_claims=self.ADDITIONAL_CLAIMS
+        )
+
+        assert credentials._signer.key_id == info["private_key_id"]
+        assert credentials._issuer == info["client_email"]
+        assert credentials._subject == self.SUBJECT
+        assert credentials._additional_claims == self.ADDITIONAL_CLAIMS
+
+    def test_from_service_account_file(self):
+        info = test_jwt.SERVICE_ACCOUNT_INFO.copy()
+
+        credentials = jwt_async.OnDemandCredentials.from_service_account_file(
+            test_jwt.SERVICE_ACCOUNT_JSON_FILE
+        )
+
+        assert credentials._signer.key_id == info["private_key_id"]
+        assert credentials._issuer == info["client_email"]
+        assert credentials._subject == info["client_email"]
+
+    def test_from_service_account_file_args(self):
+        info = test_jwt.SERVICE_ACCOUNT_INFO.copy()
+
+        credentials = jwt_async.OnDemandCredentials.from_service_account_file(
+            test_jwt.SERVICE_ACCOUNT_JSON_FILE,
+            subject=self.SUBJECT,
+            additional_claims=self.ADDITIONAL_CLAIMS,
+        )
+
+        assert credentials._signer.key_id == info["private_key_id"]
+        assert credentials._issuer == info["client_email"]
+        assert credentials._subject == self.SUBJECT
+        assert credentials._additional_claims == self.ADDITIONAL_CLAIMS
+
+    def test_from_signing_credentials(self):
+        jwt_from_signing = self.credentials.from_signing_credentials(self.credentials)
+        jwt_from_info = jwt_async.OnDemandCredentials.from_service_account_info(
+            test_jwt.SERVICE_ACCOUNT_INFO
+        )
+
+        assert isinstance(jwt_from_signing, jwt_async.OnDemandCredentials)
+        assert jwt_from_signing._signer.key_id == jwt_from_info._signer.key_id
+        assert jwt_from_signing._issuer == jwt_from_info._issuer
+        assert jwt_from_signing._subject == jwt_from_info._subject
+
+    def test_default_state(self):
+        # Credentials are *always* valid.
+        assert self.credentials.valid
+        # Credentials *never* expire.
+        assert not self.credentials.expired
+
+    def test_with_claims(self):
+        new_claims = {"meep": "moop"}
+        new_credentials = self.credentials.with_claims(additional_claims=new_claims)
+
+        assert new_credentials._signer == self.credentials._signer
+        assert new_credentials._issuer == self.credentials._issuer
+        assert new_credentials._subject == self.credentials._subject
+        assert new_credentials._additional_claims == new_claims
+
+    def test_with_quota_project(self):
+        quota_project_id = "project-foo"
+        new_credentials = self.credentials.with_quota_project(quota_project_id)
+
+        assert new_credentials._signer == self.credentials._signer
+        assert new_credentials._issuer == self.credentials._issuer
+        assert new_credentials._subject == self.credentials._subject
+        assert new_credentials._additional_claims == self.credentials._additional_claims
+        assert new_credentials._quota_project_id == quota_project_id
+
+    def test_sign_bytes(self):
+        to_sign = b"123"
+        signature = self.credentials.sign_bytes(to_sign)
+        assert crypt.verify_signature(to_sign, signature, test_jwt.PUBLIC_CERT_BYTES)
+
+    def test_signer(self):
+        assert isinstance(self.credentials.signer, crypt.RSASigner)
+
+    def test_signer_email(self):
+        assert (
+            self.credentials.signer_email
+            == test_jwt.SERVICE_ACCOUNT_INFO["client_email"]
+        )
+
+    def _verify_token(self, token):
+        payload = jwt_async.decode(token, test_jwt.PUBLIC_CERT_BYTES)
+        assert payload["iss"] == self.SERVICE_ACCOUNT_EMAIL
+        return payload
+
+    def test_refresh(self):
+        with pytest.raises(exceptions.RefreshError):
+            self.credentials.refresh(None)
+
+    def test_before_request(self):
+        headers = {}
+
+        self.credentials.before_request(
+            None, "GET", "http://example.com?a=1#3", headers
+        )
+
+        _, token = headers["authorization"].split(" ")
+        payload = self._verify_token(token)
+
+        assert payload["aud"] == "http://example.com"
+
+        # Making another request should re-use the same token.
+        self.credentials.before_request(None, "GET", "http://example.com?b=2", headers)
+
+        _, new_token = headers["authorization"].split(" ")
+
+        assert new_token == token
+
+    def test_expired_token(self):
+        self.credentials._cache["audience"] = (
+            mock.sentinel.token,
+            datetime.datetime.min,
+        )
+
+        token = self.credentials._get_jwt_for_audience("audience")
+
+        assert token != mock.sentinel.token
diff --git a/tests_async/transport/__init__.py b/tests_async/transport/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests_async/transport/__init__.py
diff --git a/tests_async/transport/async_compliance.py b/tests_async/transport/async_compliance.py
new file mode 100644
index 0000000..9c4b173
--- /dev/null
+++ b/tests_async/transport/async_compliance.py
@@ -0,0 +1,133 @@
+# 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.
+
+import time
+
+import flask
+import pytest
+from pytest_localserver.http import WSGIServer
+from six.moves import http_client
+
+from google.auth import exceptions
+from tests.transport import compliance
+
+
+class RequestResponseTests(object):
+    @pytest.fixture(scope="module")
+    def server(self):
+        """Provides a test HTTP server.
+
+        The test server is automatically created before
+        a test and destroyed at the end. The server is serving a test
+        application that can be used to verify requests.
+        """
+        app = flask.Flask(__name__)
+        app.debug = True
+
+        # pylint: disable=unused-variable
+        # (pylint thinks the flask routes are unusued.)
+        @app.route("/basic")
+        def index():
+            header_value = flask.request.headers.get("x-test-header", "value")
+            headers = {"X-Test-Header": header_value}
+            return "Basic Content", http_client.OK, headers
+
+        @app.route("/server_error")
+        def server_error():
+            return "Error", http_client.INTERNAL_SERVER_ERROR
+
+        @app.route("/wait")
+        def wait():
+            time.sleep(3)
+            return "Waited"
+
+        # pylint: enable=unused-variable
+
+        server = WSGIServer(application=app.wsgi_app)
+        server.start()
+        yield server
+        server.stop()
+
+    @pytest.mark.asyncio
+    async def test_request_basic(self, server):
+        request = self.make_request()
+        response = await request(url=server.url + "/basic", method="GET")
+        assert response.status == http_client.OK
+        assert response.headers["x-test-header"] == "value"
+
+        # Use 13 as this is the length of the data written into the stream.
+
+        data = await response.data.read(13)
+        assert data == b"Basic Content"
+
+    @pytest.mark.asyncio
+    async def test_request_basic_with_http(self, server):
+        request = self.make_with_parameter_request()
+        response = await request(url=server.url + "/basic", method="GET")
+        assert response.status == http_client.OK
+        assert response.headers["x-test-header"] == "value"
+
+        # Use 13 as this is the length of the data written into the stream.
+
+        data = await response.data.read(13)
+        assert data == b"Basic Content"
+
+    @pytest.mark.asyncio
+    async def test_request_with_timeout_success(self, server):
+        request = self.make_request()
+        response = await request(url=server.url + "/basic", method="GET", timeout=2)
+
+        assert response.status == http_client.OK
+        assert response.headers["x-test-header"] == "value"
+
+        data = await response.data.read(13)
+        assert data == b"Basic Content"
+
+    @pytest.mark.asyncio
+    async def test_request_with_timeout_failure(self, server):
+        request = self.make_request()
+
+        with pytest.raises(exceptions.TransportError):
+            await request(url=server.url + "/wait", method="GET", timeout=1)
+
+    @pytest.mark.asyncio
+    async def test_request_headers(self, server):
+        request = self.make_request()
+        response = await request(
+            url=server.url + "/basic",
+            method="GET",
+            headers={"x-test-header": "hello world"},
+        )
+
+        assert response.status == http_client.OK
+        assert response.headers["x-test-header"] == "hello world"
+
+        data = await response.data.read(13)
+        assert data == b"Basic Content"
+
+    @pytest.mark.asyncio
+    async def test_request_error(self, server):
+        request = self.make_request()
+
+        response = await request(url=server.url + "/server_error", method="GET")
+        assert response.status == http_client.INTERNAL_SERVER_ERROR
+        data = await response.data.read(5)
+        assert data == b"Error"
+
+    @pytest.mark.asyncio
+    async def test_connection_error(self):
+        request = self.make_request()
+
+        with pytest.raises(exceptions.TransportError):
+            await request(url="http://{}".format(compliance.NXDOMAIN), method="GET")
diff --git a/tests_async/transport/test_aiohttp_requests.py b/tests_async/transport/test_aiohttp_requests.py
new file mode 100644
index 0000000..a64a4ee
--- /dev/null
+++ b/tests_async/transport/test_aiohttp_requests.py
@@ -0,0 +1,254 @@
+# 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.
+
+import aiohttp
+from aioresponses import aioresponses, core
+import mock
+import pytest
+from tests_async.transport import async_compliance
+
+import google.auth._credentials_async
+from google.auth.transport import _aiohttp_requests as aiohttp_requests
+import google.auth.transport._mtls_helper
+
+
+class TestCombinedResponse:
+    @pytest.mark.asyncio
+    async def test__is_compressed(self):
+        response = core.CallbackResult(headers={"Content-Encoding": "gzip"})
+        combined_response = aiohttp_requests._CombinedResponse(response)
+        compressed = combined_response._is_compressed()
+        assert compressed
+
+    def test__is_compressed_not(self):
+        response = core.CallbackResult(headers={"Content-Encoding": "not"})
+        combined_response = aiohttp_requests._CombinedResponse(response)
+        compressed = combined_response._is_compressed()
+        assert not compressed
+
+    @pytest.mark.asyncio
+    async def test_raw_content(self):
+
+        mock_response = mock.AsyncMock()
+        mock_response.content.read.return_value = mock.sentinel.read
+        combined_response = aiohttp_requests._CombinedResponse(response=mock_response)
+        raw_content = await combined_response.raw_content()
+        assert raw_content == mock.sentinel.read
+
+        # Second call to validate the preconfigured path.
+        combined_response._raw_content = mock.sentinel.stored_raw
+        raw_content = await combined_response.raw_content()
+        assert raw_content == mock.sentinel.stored_raw
+
+    @pytest.mark.asyncio
+    async def test_content(self):
+        mock_response = mock.AsyncMock()
+        mock_response.content.read.return_value = mock.sentinel.read
+        combined_response = aiohttp_requests._CombinedResponse(response=mock_response)
+        content = await combined_response.content()
+        assert content == mock.sentinel.read
+
+    @mock.patch(
+        "google.auth.transport._aiohttp_requests.urllib3.response.MultiDecoder.decompress",
+        return_value="decompressed",
+        autospec=True,
+    )
+    @pytest.mark.asyncio
+    async def test_content_compressed(self, urllib3_mock):
+        rm = core.RequestMatch(
+            "url", headers={"Content-Encoding": "gzip"}, payload="compressed"
+        )
+        response = await rm.build_response(core.URL("url"))
+
+        combined_response = aiohttp_requests._CombinedResponse(response=response)
+        content = await combined_response.content()
+
+        urllib3_mock.assert_called_once()
+        assert content == "decompressed"
+
+
+class TestResponse:
+    def test_ctor(self):
+        response = aiohttp_requests._Response(mock.sentinel.response)
+        assert response._response == mock.sentinel.response
+
+    @pytest.mark.asyncio
+    async def test_headers_prop(self):
+        rm = core.RequestMatch("url", headers={"Content-Encoding": "header prop"})
+        mock_response = await rm.build_response(core.URL("url"))
+
+        response = aiohttp_requests._Response(mock_response)
+        assert response.headers["Content-Encoding"] == "header prop"
+
+    @pytest.mark.asyncio
+    async def test_status_prop(self):
+        rm = core.RequestMatch("url", status=123)
+        mock_response = await rm.build_response(core.URL("url"))
+        response = aiohttp_requests._Response(mock_response)
+        assert response.status == 123
+
+    @pytest.mark.asyncio
+    async def test_data_prop(self):
+        mock_response = mock.AsyncMock()
+        mock_response.content.read.return_value = mock.sentinel.read
+        response = aiohttp_requests._Response(mock_response)
+        data = await response.data.read()
+        assert data == mock.sentinel.read
+
+
+class TestRequestResponse(async_compliance.RequestResponseTests):
+    def make_request(self):
+        return aiohttp_requests.Request()
+
+    def make_with_parameter_request(self):
+        http = aiohttp.ClientSession(auto_decompress=False)
+        return aiohttp_requests.Request(http)
+
+    def test_unsupported_session(self):
+        http = aiohttp.ClientSession(auto_decompress=True)
+        with pytest.raises(ValueError):
+            aiohttp_requests.Request(http)
+
+    def test_timeout(self):
+        http = mock.create_autospec(
+            aiohttp.ClientSession, instance=True, _auto_decompress=False
+        )
+        request = aiohttp_requests.Request(http)
+        request(url="http://example.com", method="GET", timeout=5)
+
+
+class CredentialsStub(google.auth._credentials_async.Credentials):
+    def __init__(self, token="token"):
+        super(CredentialsStub, self).__init__()
+        self.token = token
+
+    def apply(self, headers, token=None):
+        headers["authorization"] = self.token
+
+    def refresh(self, request):
+        self.token += "1"
+
+
+class TestAuthorizedSession(object):
+    TEST_URL = "http://example.com/"
+    method = "GET"
+
+    def test_constructor(self):
+        authed_session = aiohttp_requests.AuthorizedSession(mock.sentinel.credentials)
+        assert authed_session.credentials == mock.sentinel.credentials
+
+    def test_constructor_with_auth_request(self):
+        http = mock.create_autospec(
+            aiohttp.ClientSession, instance=True, _auto_decompress=False
+        )
+        auth_request = aiohttp_requests.Request(http)
+
+        authed_session = aiohttp_requests.AuthorizedSession(
+            mock.sentinel.credentials, auth_request=auth_request
+        )
+
+        assert authed_session._auth_request == auth_request
+
+    @pytest.mark.asyncio
+    async def test_request(self):
+        with aioresponses() as mocked:
+            credentials = mock.Mock(wraps=CredentialsStub())
+
+            mocked.get(self.TEST_URL, status=200, body="test")
+            session = aiohttp_requests.AuthorizedSession(credentials)
+            resp = await session.request(
+                "GET",
+                "http://example.com/",
+                headers={"Keep-Alive": "timeout=5, max=1000", "fake": b"bytes"},
+            )
+
+            assert resp.status == 200
+            assert "test" == await resp.text()
+
+            await session.close()
+
+    @pytest.mark.asyncio
+    async def test_ctx(self):
+        with aioresponses() as mocked:
+            credentials = mock.Mock(wraps=CredentialsStub())
+            mocked.get("http://test.example.com", payload=dict(foo="bar"))
+            session = aiohttp_requests.AuthorizedSession(credentials)
+            resp = await session.request("GET", "http://test.example.com")
+            data = await resp.json()
+
+            assert dict(foo="bar") == data
+
+            await session.close()
+
+    @pytest.mark.asyncio
+    async def test_http_headers(self):
+        with aioresponses() as mocked:
+            credentials = mock.Mock(wraps=CredentialsStub())
+            mocked.post(
+                "http://example.com",
+                payload=dict(),
+                headers=dict(connection="keep-alive"),
+            )
+
+            session = aiohttp_requests.AuthorizedSession(credentials)
+            resp = await session.request("POST", "http://example.com")
+
+            assert resp.headers["Connection"] == "keep-alive"
+
+            await session.close()
+
+    @pytest.mark.asyncio
+    async def test_regexp_example(self):
+        with aioresponses() as mocked:
+            credentials = mock.Mock(wraps=CredentialsStub())
+            mocked.get("http://example.com", status=500)
+            mocked.get("http://example.com", status=200)
+
+            session1 = aiohttp_requests.AuthorizedSession(credentials)
+
+            resp1 = await session1.request("GET", "http://example.com")
+            session2 = aiohttp_requests.AuthorizedSession(credentials)
+            resp2 = await session2.request("GET", "http://example.com")
+
+            assert resp1.status == 500
+            assert resp2.status == 200
+
+            await session1.close()
+            await session2.close()
+
+    @pytest.mark.asyncio
+    async def test_request_no_refresh(self):
+        credentials = mock.Mock(wraps=CredentialsStub())
+        with aioresponses() as mocked:
+            mocked.get("http://example.com", status=200)
+            authed_session = aiohttp_requests.AuthorizedSession(credentials)
+            response = await authed_session.request("GET", "http://example.com")
+            assert response.status == 200
+            assert credentials.before_request.called
+            assert not credentials.refresh.called
+
+            await authed_session.close()
+
+    @pytest.mark.asyncio
+    async def test_request_refresh(self):
+        credentials = mock.Mock(wraps=CredentialsStub())
+        with aioresponses() as mocked:
+            mocked.get("http://example.com", status=401)
+            mocked.get("http://example.com", status=200)
+            authed_session = aiohttp_requests.AuthorizedSession(credentials)
+            response = await authed_session.request("GET", "http://example.com")
+            assert credentials.refresh.called
+            assert response.status == 200
+
+            await authed_session.close()