2

I am trying to initialize firebase-admin on my Flask API. Based off the documentation, the method initialize_app can take a dictionary as an input as seen here:

https://github.com/firebase/firebase-admin-python/blob/6d826fd15c87db62f7501f569b6e0a762d17a05e/firebase_admin/credentials.py#L85

That said, I structured my code as follows:

import firebase_admin
from firebase_admin import auth, credentials

...

firebase_admin.initialize_app({ \
    credentials.Certificate({ \
        "type": "service_account", \
        "project_id": os.environ.get('FIREBASE_PROJECT_ID'), \
        "private_key_id": os.environ.get('PRIVATE_KEY_ID'), \
        "private_key": os.environ.get('FIREBASE_PRIVATE_KEY').replace('\\n', '\n'), \
        "client_email": os.environ.get('FIREBASE_CLIENT_EMAIL'), \
        "client_id": os.environ.get('CLIENT_ID'), \
        "auth_uri": os.environ.get('AUTH_URI'), \
        "token_uri": os.environ.get('TOKEN_URI'), \
        "auth_provider_x509_cert_url": os.environ.get('AUTH_PROVIDER_X509_CERT_URL'), \
        "client_x509_cert_url": os.environ.get('CLIENT_X509_CERT_URL'), \
    }), 
})

Now, I'm getting this error:

ValueError: Illegal Firebase credential provided. App must be initialized with a valid credential instance.

I would ideally want to set up the application like this as I would prefer not to store the serviceAccount.json on the cloud. Many of the examples I find are simply doing what I don't want to do.

What am I missing here?

Edit:

I am using the standard export <property_name>="..." on my Mac OS terminal which I presume is the same as Linux environment. As a result, os.environ.get(<property_name>) gets the corresponding value.

For reference:

I am trying to do the same thing as this:

https://www.benmvp.com/blog/initializing-firebase-admin-node-sdk-env-vars/

But with Python

Edit:

Looking at the source code here:

https://github.com/firebase/firebase-admin-python/blob/6d826fd15c87db62f7501f569b6e0a762d17a05e/firebase_admin/__init__.py#L209

It appears that the exception is being thrown here. However, in my Flask API, I have the following:

cert = { \
        "type": "service_account", \
        "project_id": os.environ.get('FIREBASE_PROJECT_ID'), \
        "private_key_id": os.environ.get('PRIVATE_KEY_ID'), \
        "private_key": os.environ.get('FIREBASE_PRIVATE_KEY').replace('\\n', '\n'), \
        "client_email": os.environ.get('FIREBASE_CLIENT_EMAIL'), \
        "client_id": os.environ.get('CLIENT_ID'), \
        "auth_uri": os.environ.get('AUTH_URI'), \
        "token_uri": os.environ.get('TOKEN_URI'), \
        "auth_provider_x509_cert_url": os.environ.get('AUTH_PROVIDER_X509_CERT_URL'), \
        "client_x509_cert_url": os.environ.get('CLIENT_X509_CERT_URL'), \
    

print(type(credentials.Certificate(cert)), isinstance(credentials.Certificate(cert), credentials.Certificate), isinstance(credentials.Certificate(cert), credentials.Base))

To which the output is:

<class 'firebase_admin.credentials.Certificate'> True True

This doesn't make sense.. Since the following block:

if not isinstance(credential, credentials.Base):

Runs if isinstance(credential, credentials.Base) is false. But I have the values as true.

jeff
  • 490
  • 1
  • 6
  • 21
  • We can't be sure that all of your calls to `os.environ.get` are returning what you expect. – Doug Stevenson Apr 14 '22 at 22:32
  • Hmm, what do you mean by that? @DougStevenson – jeff Apr 14 '22 at 22:33
  • One of the values could be incorrect and causing problems. If that's the case we can't tell you what that problem is because we can't see the data. – Doug Stevenson Apr 14 '22 at 22:40
  • Hmm, but do I have the correct setup for initializing the firebase admin sdk? @DougStevenson – jeff Apr 14 '22 at 22:41
  • If you want a code review, you should instead post to [a different Stack Exchange](https://codereview.stackexchange.com/). Stack Overflow is only for questions where you can provide a [complete minimal example](https://stackoverflow.com/help/minimal-reproducible-example) that anyone can reproduce with the info you provide. We don't have enough information reproduce this error. – Doug Stevenson Apr 14 '22 at 22:46
  • From a quick look at a reference firebase project, you are missing the `auth_provider_x509_cert_url` key inside the json. Besides, why did you choose to separate all the values of the json file? Normally when you get the json from firebase, it is given as a single file, complete with all the fields required. Your insistence to split out all the fields is what has led to this misconfiguration. For me, I just do `json.loads(os.getenv("FIREBASE_CREDENTIALS", "{}"))` to get the credential into python, where `FIREBASE_CREDENTIAL` is basically the contents of the json file in an environment variable – smac89 Apr 14 '22 at 23:31
  • @smac89 I forgot to update the post, but it's done now. Thanks! However, that doesn't solve my issue. I mean what you're doing is essentially the same thing as me, no? – jeff Apr 14 '22 at 23:44
  • Pretty much looks the same. I have `initialize_app( credential=credentials.Certificate(service_account_key), )`, where `service_account_key` is the json object I get from `json.loads(...)` like above. I can only suggest that you take your original json file, convert it to string online, and attempt loading it the same way I've shown. If that succeeds, then it might just be a simple typo in the environment variable values. Also the escape character (`\ `) is not needed. – smac89 Apr 15 '22 at 00:13
  • @smac89 Hey thanks. What you had pretty much helped me solve this annoying issue. – jeff Apr 15 '22 at 01:18

3 Answers3

3

Thanks to, smac89, for pointing me in the right direction.

The change is very minor to make it work:

firebase_admin.initialize_app({ \
    credential=credentials.Certificate({ \  # <------- This change
        "type": "service_account", \
        "project_id": os.environ.get('FIREBASE_PROJECT_ID'), \
        "private_key_id": os.environ.get('PRIVATE_KEY_ID'), \
        "private_key": os.environ.get('FIREBASE_PRIVATE_KEY').replace('\\n', '\n'), \
        "client_email": os.environ.get('FIREBASE_CLIENT_EMAIL'), \
        "client_id": os.environ.get('CLIENT_ID'), \
        "auth_uri": os.environ.get('AUTH_URI'), \
        "token_uri": os.environ.get('TOKEN_URI'), \
        "auth_provider_x509_cert_url": os.environ.get('AUTH_PROVIDER_X509_CERT_URL'), \
        "client_x509_cert_url": os.environ.get('CLIENT_X509_CERT_URL'), \
    }), 
})

The method signature for initialize_app is:

def initialize_app(credential=None, options=None, name=_DEFAULT_APP_NAME):

By not specifying the input to be credential, the input was not read correctly. Guess I learnt something new about Python default values. :P

jeff
  • 490
  • 1
  • 6
  • 21
  • Thanks for sharing. I learned something too! Found another typo: It should be `firebase_admin.initialize_app(credential=...)`. – smac89 Apr 15 '22 at 05:21
0

This answer was quite useful for me with a related problem. I was ok setting the key content as a secret environment variable. However, I wasn't sure how to set it without constructing the dictionary. In the end, I could just json.loads() the content of the secret environment key, and it worked. I thought it was worth sharing this:

import json, os
import firebase_admin
from firebase_admin.credentials import Certificate

key_dict = json.loads(
    os.environ["FIREBASE_SERVICE_ACCOUNT_CREDENTIALS_FROM_ENV"]
)

firebase_admin.initialize_app(Certificate(key_dict))
Oisin
  • 1,483
  • 12
  • 15
0

Here are a few things to consider when loading a service account with environment variables. Notably if you get the error Caused by: "No key could be detected."

  • When copy/pasting the private key, you want to do it directly from the serviceAccount.json file to the file that contains your environment configuration to avoid bad formatting.

  • If you are using a .yml/Jinja2 Ansible variable, you might want to try modifying the private key's \n by \\n (inspect the output of your private key)

To make sure the key is actually valid, I used pycryptodome to validate the key. While it is not necessary, it is still good to have.

Here's an example:

import os
import firebase_admin
from firebase_admin import credentials

from app.core.config import settings
from Crypto.PublicKey import RSA


def init_firebase():  
    private_key_raw = os.environ.get("FIREBASE_PRIVATE_KEY")
    private_key = RSA.importKey(private_key_raw)
    info = {
        # firebase credential details here
        "private_key": private_key.exportKey().decode("utf-8"),
    }
    creds = credentials.Certificate(info)
    firebase_admin.initialize_app(creds)

Note that you might encounter some solutions advising you to do something akin to credentials.Certificate(json.loads(json.dumps(info))), telling you that Certificate does not support anything else than JSON files.

However as of version 5.2.0 (my current version), the source file credentials.py of the firebase_admin package specifies that dict type is supported:

To instantiate a credential from a certificate file, either specify the file path or a dict representing the parsed contents of the file.

Anthony
  • 119
  • 2
  • 4