Repository: firebase/firebase-admin-python

Issue:
Illegal Firebase credential provided. App must be initialized with a valid credential instance error - when using another type of `google.auth.Credential` object with the SDK.
Hi there, I am having some issues using the `firebase_admin` python client with a credential object from outside the SDK.

In my use case, I want to initialize the `firebase_admin` SDK using a Service Account impersonation credential to list users within the firebase environment.

To do this I tried the following:

```python
import google.oauth2.credentials
import google.auth.impersonated_credentials

source_credentials = google.oauth2.credentials.Credentials(token=id_token)
credentials = google.auth.impersonated_credentials.Credentials(
    source_credentials=source_credentials,
    target_principal=my_service_account_email,
    target_scopes=target_scopes,
    lifetime=500
)

app_options = {'projectId': project_id}
firebase_app = firebase_admin.initialize_app(
  credential=firebase_credential, options=app_options)
```

When running `initialize_app` it returns an exception - `Illegal Firebase credential provided. App must be initialized with a valid credential instance`.

Looking into the `firebase-admin-python` source code I was able to find the source of the problem. In `firebase_admin/__init__.py` line 209 the code checks whether the supplied `credentials` object is an instance of `firebase_admin.credentials.Base`. If not, then the SDK returns the exception.

I don't understand why this is the case. Why does `firebase-admin-python` have its own credentials class which seems to just wrap `google.oauth2.credentials` objects? Why wouldn't the Firebase SDK accept a `google.oauth2.credentials` object directly?

I was able to work around it by hacking the `ApplicationDefault` class that comes with the `firebase-admin-python` SDK, however this seems a bit backwards.

```python
firebase_credential = firebase_admin.credentials.ApplicationDefault()
firebase_credential._g_credential = credentials
firebase_credential._project_id = SOURCE_PROJECT
```

In my opinion it would be better if the firebase SDK just made use of the `google.auth` module which has a much better implementation.

Hints from issue comments:
@google-oss-bot: I couldn't figure out how to label this issue, so I've labeled it for a human to triage. Hang tight.@luka-mikec: I'm running into the same issue of firebase_admin not accepting impersonated_credentials.Credentials, is there an ETA for when this might be fixed?@dinvlad: Has anyone figured out another solution?@CoatedMoose: The illegal credential issue is caused by [this check](https://github.com/firebase/firebase-admin-python/blob/0d498a65dc7d055361b8f8e30053f4a117f266f1/firebase_admin/__init__.py#L211-L213).

I was trying to initialize with anonymous credentials for some unit/integration tests, and ended up with this:

```python
from firebase_admin import credentials
from google.auth.credentials import AnonymousCredentials


class _OfflineGCPCredentials(credentials.Base):
    def get_credential(self):
        return AnonymousCredentials()
```

My guess is you should be able to put the creation of the impersonated credentials in the `get_credentials` method. I think that would be better than using the private API (directly updating `_g_credential`, `_project_id`, etc).

I agree with the initial issue though - I can't see why the API won't accept `google.auth` credentials directly. Adding the check for the base class `google.auth.credentials.Credentials`, and wrapping it with a `firebase_admin.credentials.Base` seems like an easy win.

Solution patch:
diff --git a/firebase_admin/__init__.py b/firebase_admin/__init__.py
index 0ca82ec..7bb9c59 100644
--- a/firebase_admin/__init__.py
+++ b/firebase_admin/__init__.py
@@ -18,6 +18,7 @@ import json
 import os
 import threading
 
+from google.auth.credentials import Credentials as GoogleAuthCredentials
 from google.auth.exceptions import DefaultCredentialsError
 from firebase_admin import credentials
 from firebase_admin.__about__ import __version__
@@ -208,10 +209,13 @@ class App:
                              'non-empty string.'.format(name))
         self._name = name
 
-        if not isinstance(credential, credentials.Base):
+        if isinstance(credential, GoogleAuthCredentials):
+            self._credential = credentials._ExternalCredentials(credential) # pylint: disable=protected-access
+        elif isinstance(credential, credentials.Base):
+            self._credential = credential
+        else:
             raise ValueError('Illegal Firebase credential provided. App must be initialized '
                              'with a valid credential instance.')
-        self._credential = credential
         self._options = _AppOptions(options)
         self._lock = threading.RLock()
         self._services = {}
diff --git a/firebase_admin/credentials.py b/firebase_admin/credentials.py
index 5477e1c..7506002 100644
--- a/firebase_admin/credentials.py
+++ b/firebase_admin/credentials.py
@@ -18,6 +18,7 @@ import json
 import pathlib
 
 import google.auth
+from google.auth.credentials import Credentials as GoogleAuthCredentials
 from google.auth.transport import requests
 from google.oauth2 import credentials
 from google.oauth2 import service_account
@@ -58,6 +59,19 @@ class Base:
         """Returns the Google credential instance used for authentication."""
         raise NotImplementedError
 
+class _ExternalCredentials(Base):
+    """A wrapper for google.auth.credentials.Credentials typed credential instances"""
+
+    def __init__(self, credential: GoogleAuthCredentials):
+        super(_ExternalCredentials, self).__init__()
+        self._g_credential = credential
+
+    def get_credential(self):
+        """Returns the underlying Google Credential
+
+        Returns:
+          google.auth.credentials.Credentials: A Google Auth credential instance."""
+        return self._g_credential
 
 class Certificate(Base):
     """A credential initialized from a JSON certificate keyfile."""


Test patch:
diff --git a/tests/test_app.py b/tests/test_app.py
index 4233d58..5b20366 100644
--- a/tests/test_app.py
+++ b/tests/test_app.py
@@ -246,6 +246,16 @@ class TestFirebaseApp:
         with pytest.raises(ValueError):
             firebase_admin.initialize_app(app_credential, name='myApp')
 
+    def test_app_init_with_google_auth_cred(self):
+        cred = testutils.MockGoogleCredential()
+        assert isinstance(cred, credentials.GoogleAuthCredentials)
+        app = firebase_admin.initialize_app(cred)
+        assert cred is app.credential.get_credential()
+        assert isinstance(app.credential, credentials.Base)
+        assert isinstance(app.credential, credentials._ExternalCredentials)
+        with pytest.raises(ValueError):
+            firebase_admin.initialize_app(app_credential)
+
     @pytest.mark.parametrize('cred', invalid_credentials)
     def test_app_init_with_invalid_credential(self, cred):
         with pytest.raises(ValueError):