Skip to content

Firebase for Django Rest Framework

🎉 This package is for those who want to avoid vendor lock-in with Firebase, but still use it for authentication in their DRF project. 🎉

Some features:

  • Firebase Authentication APIs.
  • Sign in with OAuth Providers.
  • Multi-factor authentication (MFA) for Email/Password provider.
  • No vendor lock-in, more control to the Developer.

Currently, OAuth is working only with Google, Facebook and GitHub 😢


Installation

You can install the package from PyPI by running the following command:

sh
pip install dj-rest-firebase

Initial Setup

Firebase Setup

  • Create a Firebase project and then a web app inside the Firebase project. Make a note of the Web API Key available under Project Settings ➡ General tab. You'll need this in Django Setup.
    • There are many articles on the web that explain how to create a Firebase project and an app inside it.
  • Navigate to Build ➡ Authentication ➡ Sign-in method and add/enable your preferred providers.
    • There are many articles on the web that explain how to generate Client ID and Client Secret for each OAuth provider.
    • NOTE: In case of Google provider, the Client ID and Client Secret are available under "Web SDK Configuration".
    • NOTE: Without this setup, you'll not be able to use Firebase authentication. You'll need to have at-least one provider added.
  • Navigate to Build ➡ Authentication ➡ Settings and customize User Account Management settings to your preference.
  • Also, under the same Settings tab, modify Authorized Domains.
  • OPTIONAL: Change Email templates under Build ➡ Authentication ➡ Templates tab. This tab defines which Email ID to use for sending emails such as verification links etc. and how the Email template looks like.
    • NOTE: The Email ID can be configured under SMTP Settings option.

Django Setup

  • Add this package to INSTALLED_APPS list in settings.py file.
python
INSTALLED_APPS = [
    ...,
    'dj_rest_firebase'
]
  • Also set the following attributes in settings.py file.
    • NOTE: DRF_MFA_ENABLED is OPTIONAL and add it only if you need multi-factor authentication (MFA).
    • NOTE: DRF_OAUTH_CALLBACK_URL will be used only if you don't pass a callback URL explicitly in certain functions (see Functions usage below). This setting is useful when your callback URL is same for all OAuth providers.
python
DRF_MFA_ENABLED = True
DRF_OAUTH_CALLBACK_URL = "https://<YOUR CALLBACK URL HERE>/"
  • Create an environment variable named FIREBASE_API_KEY and set it to the Web API Key value taken from Firebase Setup above.
  • Run Django migration commands:
    • This should create a Django model called FirebaseMFASecret.
    • This model will be unused if MFA is not enabled.
sh
python manage.py makemigrations
python manage.py migrate

Final Setup

This setup is OPTIONAL if you don't have multi-factor authentication enabled.

  • Create two 16-character (16 or more) strings and store them in files safely.
  • Use these strings as replacements for encryption_key and encryption_iv respectively in the functions.
    • NOTE: These keys are used for encrypting the MFA Secrets before saving them in the Django model, as an added security.
    • NOTE: The same keys are used to de-crypt the encrypted secrets from Django model.

Caution: If you lose these keys, the email/password authentication will not work for any user.


Release Notes

VersionRelease DateDetails
0.6.6 (Current)August 2022Firebase Auth APIs
0.7.5XYZ 202XSupport for Twitter, Yahoo, Microsoft and Apple OAuth
FutureXYZ 202XFirebase Storage & Firestore APIs

Functions Usage

Before you use these functions, it's very important that you review the Things to Consider section below to keep your application secure.

Usage Example

**All functions are available from dj_rest_firebase.auth**

py
from dj_rest_firebase.auth import fetch_providers_for_email


print(str(fetch_providers_for_email("user@example.com")))

get_mfa_secret()

Purpose: To generate an MFA Secret code

Inputs: No Input Required

Response:

py
'[MFA_SECRET]'

fetch_providers_for_email()

Purpose: To fetch the auth providers info for an Email ID

Inputs:

  • email: User's Email ID.
  • (Optional) continue_uri: The URI to which the IDP redirects the user back. For this use case, this is just the current URL.

Response:

py
{
  'kind': 'identitytoolkit#CreateAuthUriResponse',
  'allProviders': ['password'],
  'registered': True,
  'sessionId': '[SESSION_ID]',
  'signinMethods': ['password']
}

signup_with_email_password()

Purpose: To sign up a new user using Email and Password

Inputs:

  • email: User's Email ID.
  • password: User's Password.
  • (Optional) mfa_secret: MFA Secret code. This is required if DRF_MFA_ENABLED is set to True.
  • (Optional) encryption_key: Encryption Key created in Final Setup above. This is required if DRF_MFA_ENABLED is set to True.
  • (Optional) encryption_iv: Encryption IV created in Final Setup above. This is required if DRF_MFA_ENABLED is set to True.

Response:

py
{
  "idToken": "[ID_TOKEN]",
  "email": "[USER_EMAIL]",
  "refreshToken": "[REFRESH_TOKEN]",
  "expiresIn": "3600",
  "localId": "[USER_ID]"
}

signin_with_email_password()

Purpose: To sign in a user using Email and Password

Inputs:

  • email: User's Email ID.
  • password: User's Password.
  • (Optional) otp: One Time Passcode (String). This is required if DRF_MFA_ENABLED is set to True.
  • (Optional) encryption_key: Encryption Key created in Final Setup above. This is required if DRF_MFA_ENABLED is set to True.
  • (Optional) encryption_iv: Encryption IV created in Final Setup above. This is required if DRF_MFA_ENABLED is set to True.

Response:

py
{
  "localId": "[USER_ID]",
  "email": "[USER_EMAIL]",
  "displayName": "",
  "idToken": "[ID_TOKEN]",
  "registered": True,
  "refreshToken": "[REFRESH_TOKEN]",
  "expiresIn": "3600"
}

signin_anonymously()

Purpose: To sign in anonymously

Inputs: No Input Required

Response:

py
{
  "idToken": "[ID_TOKEN]",
  "email": "",
  "refreshToken": "[REFRESH_TOKEN]",
  "expiresIn": "3600",
  "localId": "[USER_ID]"
}

signin_with_oauth()

Purpose: To sign in a user using OAuth Provider

Inputs:

  • auth_provider: OAuth provider name.
  • auth_code: Authorization code returned to the Callback URL by the links from this table below.
  • client_id: OAuth Provider Client ID.
  • client_secret: OAuth Provider Client Secret.
  • (Optional) callback_url: OAuth callback URL. This is REQUIRED if DRF_OAUTH_CALLBACK_URL is not set.
  • (Optional) challenge_txt: This is required only when OAuth Provider is twitter.com. This value should match <CHALLENGE_TEXT> from this table below.

Response:

py
{
  "federatedId": "https://accounts.google.com/1234567890",
  "providerId": "google.com",
  "localId": "[USER_ID]",
  "emailVerified": True,
  "email": "[USER_EMAIL]",
  "oauthIdToken": "[GOOGLE_ID_TOKEN]",
  "firstName": "John",
  "lastName": "Doe",
  "fullName": "John Doe",
  "displayName": "John Doe",
  "idToken": "[ID_TOKEN]",
  "photoUrl": "https://lh5.googleusercontent.com/.../photo.jpg",
  "refreshToken": "[REFRESH_TOKEN]",
  "expiresIn": "3600",
  "rawUserInfo": "{\"updated_time\":\"2017-02-22T01:10:57+0000\",\"gender\":\"male\", ...}"
}

send_email_verification()

Purpose: To send an email with verification link

Inputs:

  • id_token: Firebase Auth ID token.

Response:

py
{
  "success": "VERIFICATION_EMAIL_SENT"
}

confirm_email_verification()

Purpose: To confirm email verification

Inputs:

  • oob_code: Code from Verification Email.

Response:

py
{
  "localId": "[USER_ID]",
  "email": "[USER_EMAIL]",
  "passwordHash": "...",
  "providerUserInfo": [
    {
      "providerId": "password",
      "federatedId": "[USER_EMAIL]"
    }
  ]
}

get_user_data()

Purpose: To get user data from Firebase

Inputs:

  • id_token: Firebase Auth ID token.

Response:

py
{
  "users": [
    {
      "localId": "[USER_ID]",
      "email": "[USER_EMAIL]",
      "emailVerified": False,
      "displayName": "John Doe",
      "providerUserInfo": [
        {
          "providerId": "password",
          "displayName": "John Doe",
          "photoUrl": "http://localhost:8080/img1234567890/photo.png",
          "email": "[USER_EMAIL]",
        }
      ],
      "photoUrl": "https://lh5.googleusercontent.com/.../photo.jpg",
      "passwordHash": "...",
      "passwordUpdatedAt": 1.484124177E12,
      "validSince": "1484124177",
      "disabled": False,
      "lastLoginAt": "1484628946000",
      "createdAt": "1484124142000",
      "customAuth": False
    }
  ]
}

delete_account()

Purpose: To delete a user account

Inputs:

  • id_token: Firebase Auth ID token.

Response:

py
{
  "success": "ACCOUNT_DELETED"
}

update_profile()

Purpose: To delete a user account

Inputs:

  • id_token: Firebase Auth ID token.
  • display_name: User's Display Name (First Name and Last Name combined).
  • photo_url: User's Photo URL.
  • (Optional) delete_attributes: (bool) Flag to indicate whether to delete any attributes.
  • (Optional) delete_attributes_list: (list) List of attributes to delete. Acceptable list values: 'DISPLAY_NAME', 'PHOTO_URL'.

Response:

py
{
  "localId": "[USER_ID]",
  "email": "[USER_EMAIL]",
  "displayName": "John Doe",
  "photoUrl": "[http://localhost:8080/img1234567890/photo.png]",
  "passwordHash": "...",
  "providerUserInfo": [
    {
      "providerId": "password",
      "federatedId": "[USER_EMAIL]",
      "displayName": "John Doe",
      "photoUrl": "http://localhost:8080/img1234567890/photo.png"
    }
  ],
  "idToken": "[NEW_ID_TOKEN]",
  "refreshToken": "[NEW_REFRESH_TOKEN]",
  "expiresIn": "3600"
}

send_password_reset_email()

Purpose: To Email password reset link

Inputs:

  • email: User Email ID.

Response:

py
{
  "success": "PASSWORD_RESET_EMAIL_SENT"
}

verify_password_reset_code()

Purpose: To verify code from password reset Email

Inputs:

  • oob_code: Code from Password Reset Email.

Response:

py
{
  "success": "PASSWORD_RESET_CODE_VALID"
}

confirm_password_reset()

Purpose: To confirm password reset

Inputs:

  • oob_code: Code from Password Reset Email.
  • new_password: User's new password.

Response:

py
{
  "success": "PASSWORD_RESET_DONE"
}

change_email()

Purpose: To change User's Email

Inputs:

  • id_token: Firebase Auth ID token.
  • new_email: User's new Email ID.

Response:

py
{
  "localId": "[USER_ID]",
  "email": "[USER_EMAIL]",
  "passwordHash": "...",
  "providerUserInfo": [
    {
      "providerId": "password",
      "federatedId": "[USER_EMAIL]"
    }
  ],
  "idToken": "[NEW_ID_TOKEN]",
  "refreshToken": "[NEW_REFRESH_TOKEN]",
  "expiresIn": "3600"
}

change_password()

Purpose: To change User's Password

Inputs:

  • id_token: Firebase Auth ID token.
  • new_password: User's new password.

Response:

py
{
  "localId": "[USER_ID]",
  "email": "[USER_EMAIL]",
  "passwordHash": "...",
  "providerUserInfo": [
    {
      "providerId": "password",
      "federatedId": "[USER_EMAIL]"
    }
  ],
  "idToken": "[NEW_ID_TOKEN]",
  "refreshToken": "[NEW_REFRESH_TOKEN]",
  "expiresIn": "3600"
}

change_mfa_secret()

Purpose: To change User's MFA Secret

Inputs:

  • id_token: Firebase Auth ID token.
  • mfa_secret: MFA Secret code.
  • encryption_key: Encryption Key created in Final Setup above.
  • encryption_iv: Encryption IV created in Final Setup above.

Response:

py
{
  "success": "MFA_UPDATED"
}

Purpose: To link user account with Email/Password

Inputs:

  • id_token: Firebase Auth ID token.
  • email: User's Email ID.
  • new_password: User's new password.
  • (Optional) mfa_secret: MFA Secret code. This is required if DRF_MFA_ENABLED is set to True.
  • (Optional) encryption_key: Encryption Key created in Final Setup above. This is required if DRF_MFA_ENABLED is set to True.
  • (Optional) encryption_iv: Encryption IV created in Final Setup above. This is required if DRF_MFA_ENABLED is set to True.

Response:

python
{
  "localId": "[USER_ID]",
  "email": "[USER_EMAIL]",
  "displayName": "John Doe",
  "photoUrl": "https://lh5.googleusercontent.com/.../photo.jpg",
  "passwordHash": "...",
  "providerUserInfo": [
    {
      "providerId": "password",
      "federatedId": "[USER_EMAIL]"
    }
  ],
  "idToken": "[ID_TOKEN]",
  "refreshToken": "[REFRESH_TOKEN]",
  "expiresIn": "3600",
  "emailVerified": False
}

Purpose: To link user account with an OAuth provider

Inputs:

  • id_token: Firebase Auth ID token.
  • auth_provider: OAuth provider name.
  • auth_code: Authorization code returned to the Callback URL by the links from this table below.
  • client_id: OAuth Provider Client ID.
  • client_secret: OAuth Provider Client Secret.
  • (Optional) callback_url: OAuth callback URL. This is REQUIRED if DRF_OAUTH_CALLBACK_URL is not set.
  • (Optional) challenge_txt: This is required only when OAuth Provider is twitter.com. This value should match <CHALLENGE_TEXT> from this table below.

Response:

py
{
  "federatedId": "https://accounts.google.com/1234567890",
  "providerId": "google.com",
  "localId": "[USER_ID]",
  "emailVerified": True,
  "email": "[USER_EMAIL]",
  "oauthIdToken": "[GOOGLE_ID_TOKEN]",
  "firstName": "John",
  "lastName": "Doe",
  "fullName": "John Doe",
  "displayName": "John Doe",
  "idToken": "[ID_TOKEN]",
  "photoUrl": "https://lh5.googleusercontent.com/.../photo.jpg",
  "refreshToken": "[REFRESH_TOKEN]",
  "expiresIn": "3600",
  "rawUserInfo": "{\"updated_time\":\"2017-02-22T01:10:57+0000\",\"gender\":\"male\", ...}"
}

Purpose: To unlink User Account from an Auth provider

Inputs:

  • id_token: Firebase Auth ID token.
  • auth_provider: OAuth provider name.

Response:

py
{
  "localId": "[USER_ID]",
  "email": "[USER_EMAIL]",
  "displayName": "John Doe",
  "photoUrl": "https://lh5.googleusercontent.com/.../photo.jpg",
  "passwordHash": "...",
  "providerUserInfo": [
    {
      "providerId": "google.com",
      "federatedId": "1234567890",
      "displayName": "John Doe",
      "photoUrl": "https://lh5.googleusercontent.com/.../photo.jpg"
    },
    {
      "providerId": "password",
      "federatedId": "[USER_EMAIL]"
    }
  ],
  "emailVerified": "true"
}

refresh_id_token()

Purpose: To generate a new ID token using Refresh token

Inputs:

  • refresh_token: Firebase Auth Refresh token.

Response:

py
{
  "expires_in": "3600",
  "token_type": "Bearer",
  "refresh_token": "[REFRESH_TOKEN]",
  "id_token": "[ID_TOKEN]",
  "user_id": "[USER_ID]",
  "project_id": "1234567890"
}

exchange_custom_token_for_id_token()

Purpose: To exchange custom token for ID and Refresh tokens

Inputs:

  • custom_token: Firebase Auth Custom token.

Response:

py
{
  "idToken": "[ID_TOKEN]",
  "refreshToken": "[REFRESH_TOKEN]",
  "expiresIn": "3600"
}

Things to Consider

Response Codes

All functions return a dictionary with one of the following keys (and a code as their value), in case of un-successful operations:

  • error
    • Indicates failed operation
  • warning
    • This is not really an error but something to be taken care of by the application. The only possible code for this is AUTH_PROVIDER_NOT_FOUND. See below on what this means.
AUTH_PROVIDER_NOT_FOUND

If you're getting this response, it means that the user is trying to sign in using a provider that they didn't previously link.

For example, if the user signed up using Email/Password initially, but later trying to sign in using Google provider (with same Email ID). Another example, if the user signed up using Facebook provider, but later trying to sign in using GitHub provider.

In order to allow the user to perform above actions, they need to first link their providers while being signed in. That is, in the first example above, they should first sign in using Email/Password and then link Google provider, in order to use it for signing-in in the future.

ADMIN_ONLY_OPERATION

This error code indicates that some firebase configuration is not done or incomplete.

For example, you may receive this error when you try to use the function signin_anonymously(). Here, it indicates that the Anonymous provider is not enabled in Firebase.

emailVerified Boolean

When you call signup_with_email_password function or signin_with_oauth function, it returns an id_token in the success response. However, the emailVerified attribute may be returned as False.

It's very important that the email gets verified, before letting the user access any sensitive information. Not doing so can make your application insecure.

signin_with_oauth always returns emailVerified as True when the auth provider is 'google.com'

QR Code Generator (for MFA Secret)

The following link can be used to generate QR Code image, using the MFA Secret. Replace <MFA_SECRET> and <ACCOUNT_NAME>.

https://chart.apis.google.com/chart?cht=qr&chs=250x250&chld=L|2&chl=otpauth://totp/<ACCOUNT_NAME>?secret=<MFA_SECRET>

You can use following links in your frontend application to sign in with OAuth provider. Replace <CLIENT_ID>, <CALLBACK_URL> and <YOUR_STATE>.

  • <CLIENT_ID> is your OAuth provider's client id.
  • <CALLBACK_URL> is the callback URL set in your OAuth app settings.
  • <YOUR_STATE> is some state that needs to be persisted in the callback URL.
  • <CHALLENGE_TEXT> is required only for Twitter.
OAuth ProviderAuthorize Link
Googlehttps://accounts.google.com/o/oauth2/auth/oauthchooseaccount?prompt=select_account&client_id=<CLIENT_ID>&redirect_uri=<CALLBACK_URL>&scope=profile%20email&response_type=code&flowName=GeneralOAuthFlow&state=<YOUR_STATE>
Facebookhttps://www.facebook.com/v14.0/dialog/oauth?client_id=<CLIENT_ID>&redirect_uri=<CALLBACK_URL>&scope=email&auth_type=rerequest&state=<YOUR_STATE>
GitHubhttps://github.com/login/oauth/authorize?login&client_id=<CLIENT_ID>&redirect_uri=<CALLBACK_URL>&scope=user&state=<YOUR_STATE>
Microsofthttps://login.microsoftonline.com/common/adminconsent?client_id=<CLIENT_ID>&redirect_uri=<CALLBACK_URL>&state=<YOUR_STATE>
Applehttps://appleid.apple.com/auth/authorize?response_type=code id_token&response_mode=form_post&client_id=<CLIENT_ID>&scope=name email&redirect_uri=<CALLBACK_URL>&state=<YOUR_STATE>
Yahoohttps://api.login.yahoo.com/oauth2/request_auth?client_id=<CLIENT_ID>&response_type=code&redirect_uri=<CALLBACK_URL>&scope=openid&state=<YOUR_STATE>
Twitterhttps://twitter.com/i/oauth2/authorize?response_type=code&client_id=<CLIENT_ID>&redirect_uri=<CALLBACK_URL>&scope=offline.access&state=<YOUR_STATE>&code_challenge=<CHALLENGE_TEXT>&code_challenge_method=plain
Amazonhttps://www.amazon.com/ap/oa?client_id=<CLIENT_ID>&scope=profile&response_type=code&redirect_uri=<CALLBACK_URL>&state=<YOUR_STATE>
LinkedInhttps://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=<CLIENT_ID>&redirect_uri=<CALLBACK_URL>&scope=r_liteprofile%20r_emailaddress&state=<YOUR_STATE>
Reddithttps://www.reddit.com/api/v1/authorize?response_type=code&client_id=<CLIENT_ID>&redirect_uri=<CALLBACK_URL>&scope=identity&state=<YOUR_STATE>

Help

If you run into any issues while setting up or while using the package, feel free to open an issue in this GitHub repository.

This is my first PyPI package and I built it from scratch. If you like my work, please consider giving it a GitHub ⭐️️. Thanks!