# Copyright (c) 2022 Asif Arman Rahman
# Licensed under MIT (https://github.com/AsifArmanRahman/firebase/blob/main/LICENSE)
# --------------------------------------------------------------------------------------
"""
A simple python wrapper for Google's
`Firebase Authentication REST API`_
.. _Firebase Authentication REST API: https://firebase.google.com/docs/reference/rest/auth
"""
import json
import datetime
import python_jwt as jwt
import jwcrypto.jwk as jwk
from urllib.parse import parse_qs
from cryptography.hazmat.primitives.serialization import Encoding, NoEncryption, PrivateFormat
from firebase._exception import raise_detailed_error
[docs]class Auth:
""" Firebase Authentication Service
:type api_key: str
:param api_key: ``apiKey`` from Firebase configuration
:type credentials: :class:`~google.oauth2.service_account.Credentials`
:param credentials: Service Account Credentials
:type requests: :class:`~requests.Session`
:param requests: Session to make HTTP requests
:type client_secret: str or dict
:param client_secret: (Optional) File path to or the dict object
from social client secret file, defaults to :data:`None`.
"""
def __init__(self, api_key, credentials, requests, client_secret=None):
""" Constructor method """
self.api_key = api_key
self.credentials = credentials
self.requests = requests
self.current_user = None
self.provider_id = None
self.session_id = None
if client_secret:
self.client_secret = _load_client_secret(client_secret)
[docs] def authenticate_login_with_google(self):
""" Redirect the user to Google's OAuth 2.0 server to initiate
the authentication and authorization process.
:return: Google Sign In URL
:rtype: str
"""
return self.create_authentication_uri('google.com')
[docs] def create_authentication_uri(self, provider_id):
""" Creates an authentication URI for the given social
provider.
| For more details:
| |section-fetch-providers-for-email|_
.. |section-fetch-providers-for-email| replace::
Firebase Auth REST API | Fetch providers for email
.. _section-fetch-providers-for-email:
https://firebase.google.com/docs/reference/rest/auth#section-fetch-providers-for-email
:type provider_id: str
:param provider_id: The IdP ID. For white listed IdPs it's a
short domain name e.g. 'google.com', 'aol.com', 'live.net'
and 'yahoo.com'. For other OpenID IdPs it's the OP
identifier.
:return: The URI used by the IDP to authenticate the user.
:rtype: str
"""
request_ref = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/createAuthUri?key={0}".format(self.api_key)
data = {
"authFlowType": 'CODE_FLOW',
"clientId": self.client_secret['client_id'],
"providerId": provider_id,
"continueUri": self.client_secret['redirect_uris'][0],
"customParameter": {
"access_type": 'offline',
"prompt": 'select_account',
"include_granted_scopes": 'true',
}
}
headers = {"content-type": "application/json; charset=UTF-8"}
request_object = self.requests.post(request_ref, headers=headers, json=data)
raise_detailed_error(request_object)
self.provider_id = provider_id
self.session_id = request_object.json()['sessionId']
return request_object.json()['authUri']
[docs] def sign_in_with_email_and_password(self, email, password):
""" Sign in a user with an email and password.
| For more details:
| `Firebase Auth REST API | section-sign-in-email-password`_
.. _Firebase Auth REST API | section-sign-in-email-password: https://firebase.google.com/docs/reference/rest/auth#section-sign-in-email-password
:type email: str
:param email: The email the user is signing in with.
:type password: str
:param password: The password for the account.
:return: UserInfo and Firebase Auth Tokens.
:rtype: dict
"""
request_ref = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key={0}".format(self.api_key)
headers = {"content-type": "application/json; charset=UTF-8"}
data = json.dumps({"email": email, "password": password, "returnSecureToken": True})
request_object = self.requests.post(request_ref, headers=headers, data=data)
raise_detailed_error(request_object)
self.current_user = request_object.json()
return request_object.json()
[docs] def sign_in_anonymous(self):
""" Sign In Anonymously.
| For more details:
| `Firebase Auth REST API | section-sign-in-anonymously`_
.. _Firebase Auth REST API | section-sign-in-anonymously: https://firebase.google.com/docs/reference/rest/auth#section-sign-in-anonymously
:return: Firebase Auth Tokens.
:rtype: dict
"""
request_ref = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/signupNewUser?key={0}".format(self.api_key)
headers = {"content-type": "application/json; charset=UTF-8"}
data = json.dumps({"returnSecureToken": True})
request_object = self.requests.post(request_ref, headers=headers, data=data)
raise_detailed_error(request_object)
self.current_user = request_object.json()
return request_object.json()
[docs] def create_custom_token(self, uid, additional_claims=None, expiry_minutes=60):
""" Create a Firebase Auth custom token.
| For more details:
| `Firebase Documentation | Create Custom tokens`_
.. _Firebase Documentation | Create Custom tokens: https://firebase.google.com/docs/auth/admin/create-custom-tokens
:type uid: str
:param uid: The unique identifier of the user, must be a
string, between 1-36 characters long.
:type additional_claims: dict or None
:param additional_claims: Optional custom claims to include
in the Security Rules ``auth`` / ``request.auth``
variables.
:type expiry_minutes: int
:param expiry_minutes: The time, in minutes since the UNIX
epoch, at which the token expires.
Default value is 60.
:return: Firebase Auth custom token.
:rtype: str
"""
service_account_email = self.credentials.service_account_email
private_key = jwk.JWK.from_pem(self.credentials.signer._key.private_bytes(encoding=Encoding.PEM, format=PrivateFormat.PKCS8, encryption_algorithm=NoEncryption()))
payload = {
"iss": service_account_email,
"sub": service_account_email,
"aud": "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit",
"uid": uid
}
if additional_claims:
payload["claims"] = additional_claims
exp = datetime.timedelta(minutes=expiry_minutes)
return jwt.generate_jwt(payload, private_key, "RS256", exp, other_headers={'kid': self.credentials.signer._key_id})
[docs] def sign_in_with_custom_token(self, token):
""" Exchange custom token for an ID and refresh token.
| For more details:
| `Firebase Auth REST API | section-verify-custom-token`_
.. _Firebase Auth REST API | section-verify-custom-token : https://firebase.google.com/docs/reference/rest/auth#section-verify-custom-token
:type token: str
:param token: A Firebase Auth custom token from which to
create an ID and refresh token pair.
:return: Firebase Auth Tokens.
:rtype: dict
"""
request_ref = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken?key={0}".format(self.api_key) # noqa
headers = {"content-type": "application/json; charset=UTF-8"}
data = json.dumps({"returnSecureToken": True, "token": token})
request_object = self.requests.post(request_ref, headers=headers, data=data)
raise_detailed_error(request_object)
return request_object.json()
[docs] def refresh(self, refresh_token):
""" Refresh a Firebase ID token.
| For more details:
| `Firebase Auth REST API | section-refresh-token`_
.. _Firebase Auth REST API | section-refresh-token : https://firebase.google.com/docs/reference/rest/auth#section-refresh-token
:type refresh_token: str
:param refresh_token: A Firebase Auth refresh token.
:return: New (Refreshed) Firebase Auth tokens for the account.
:rtype: dict
"""
request_ref = "https://securetoken.googleapis.com/v1/token?key={0}".format(self.api_key)
headers = {"content-type": "application/json; charset=UTF-8"}
data = json.dumps({"grantType": "refresh_token", "refreshToken": refresh_token})
request_object = self.requests.post(request_ref, headers=headers, data=data)
raise_detailed_error(request_object)
request_object_json = request_object.json()
# handle weirdly formatted response
user = {
"userId": request_object_json["user_id"],
"idToken": request_object_json["id_token"],
"refreshToken": request_object_json["refresh_token"]
}
return user
[docs] def get_account_info(self, id_token):
""" Fetch user's stored account information.
| For more details:
| `Firebase Auth REST API | section-get-account-info`_
.. _Firebase Auth REST API | section-get-account-info : https://firebase.google.com/docs/reference/rest/auth#section-get-account-info
:type id_token: str
:param id_token: The Firebase ID token of the account.
:return: The account info, associated with the given
Firebase ID token.
:rtype: dict
"""
request_ref = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/getAccountInfo?key={0}".format(self.api_key)
headers = {"content-type": "application/json; charset=UTF-8"}
data = json.dumps({"idToken": id_token})
request_object = self.requests.post(request_ref, headers=headers, data=data)
raise_detailed_error(request_object)
return request_object.json()
[docs] def send_email_verification(self, id_token):
""" Send an email verification to verify email ownership.
| For more details:
| `Firebase Auth REST API | section-send-email-verification`_
.. _Firebase Auth REST API | section-send-email-verification : https://firebase.google.com/docs/reference/rest/auth#section-send-email-verification
:type id_token: str
:param id_token: The Firebase ID token of the user to verify.
:return: The email of the account associated with Firebase ID
token.
:rtype: dict
"""
request_ref = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/getOobConfirmationCode?key={0}".format(self.api_key)
headers = {"content-type": "application/json; charset=UTF-8"}
data = json.dumps({"requestType": "VERIFY_EMAIL", "idToken": id_token})
request_object = self.requests.post(request_ref, headers=headers, data=data)
raise_detailed_error(request_object)
return request_object.json()
[docs] def send_password_reset_email(self, email):
""" Send a password reset email.
| For more details:
| `Firebase Auth REST API | section-send-password-reset-email`_
.. _Firebase Auth REST API | section-send-password-reset-email: https://firebase.google.com/docs/reference/rest/auth#section-send-password-reset-email
:type email: str
:param email: User's email address.
:return: User's email address.
:rtype: dict
"""
request_ref = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/getOobConfirmationCode?key={0}".format(self.api_key)
headers = {"content-type": "application/json; charset=UTF-8"}
data = json.dumps({"requestType": "PASSWORD_RESET", "email": email})
request_object = self.requests.post(request_ref, headers=headers, data=data)
raise_detailed_error(request_object)
return request_object.json()
[docs] def verify_password_reset_code(self, reset_code, new_password):
""" Reset password using code.
| For more details:
| `Firebase Auth REST API | #section-confirm-reset-password`_
.. _Firebase Auth REST API | #section-confirm-reset-password: https://firebase.google.com/docs/reference/rest/auth#section-confirm-reset-password
:type reset_code: str
:param reset_code: The email action code sent to the user's
email for resetting the password.
:type new_password: str
:param new_password: The user's new password.
:return: User Email and Type of the email action code.
:rtype: dict
"""
request_ref = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/resetPassword?key={0}".format(self.api_key)
headers = {"content-type": "application/json; charset=UTF-8"}
data = json.dumps({"oobCode": reset_code, "newPassword": new_password})
request_object = self.requests.post(request_ref, headers=headers, data=data)
raise_detailed_error(request_object)
return request_object.json()
[docs] def create_user_with_email_and_password(self, email, password):
""" Create a new user with email and password.
| For more details:
| `Firebase Auth REST API | section-create-email-password`_
.. _Firebase Auth REST API | section-create-email-password: https://firebase.google.com/docs/reference/rest/auth#section-create-email-password
:type email: str
:param email: The email for the user to create.
:type password: str
:param password: The password for the user to create.
:return: User Email and Firebase Auth Tokens.
:rtype: dict
"""
request_ref = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/signupNewUser?key={0}".format(self.api_key)
headers = {"content-type": "application/json; charset=UTF-8"}
data = json.dumps({"email": email, "password": password, "returnSecureToken": True})
request_object = self.requests.post(request_ref, headers=headers, data=data)
raise_detailed_error(request_object)
return request_object.json()
[docs] def delete_user_account(self, id_token):
""" Delete an existing user.
| For more details:
| `Firebase Auth REST API | section-delete-account`_
.. _Firebase Auth REST API | section-delete-account: https://firebase.google.com/docs/reference/rest/auth#section-delete-account
:type id_token: str
:param id_token: The Firebase ID token of the user to
delete.
"""
request_ref = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/deleteAccount?key={0}".format(self.api_key)
headers = {"content-type": "application/json; charset=UTF-8"}
data = json.dumps({"idToken": id_token})
request_object = self.requests.post(request_ref, headers=headers, data=data)
raise_detailed_error(request_object)
return request_object.json()
[docs] def sign_in_with_oauth_credential(self, oauth2callback_url):
""" Sign In With OAuth credential.
| For more details:
| |section-sign-in-with-oauth-credential|_
.. |section-sign-in-with-oauth-credential| replace::
Firebase Auth REST API | Sign in with OAuth credential
.. _section-sign-in-with-oauth-credential:
https://firebase.google.com/docs/reference/rest/auth#section-sign-in-with-oauth-credential
:type oauth2callback_url: str
:param oauth2callback_url: The URL redirected to after
successful authorization from the provider.
:return: User account info and Firebase Auth Tokens.
:rtype: dict
"""
request_ref = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyAssertion?key={0}".format(self.api_key)
token = self._token_from_auth_url(oauth2callback_url)
data = {
'postBody': 'providerId={0}&{1}={2}'.format(self.provider_id, token['type'], token['value']),
'autoCreate': 'true',
'requestUri': self.client_secret['redirect_uris'][0],
'sessionId': self.session_id,
'returnSecureToken': 'true',
'returnRefreshToken': 'true',
'returnIdpCredential': 'false',
}
headers = {"content-type": "application/json; charset=UTF-8"}
request_object = self.requests.post(request_ref, headers=headers, json=data)
raise_detailed_error(request_object)
self.current_user = request_object.json()
return request_object.json()
def _token_from_auth_url(self, url):
""" Fetch tokens using the authorization code from given URL.
:type url: str
:param url: The URL redirected to after successful
authorization from the provider.
:return: The OAuth credential (an ID token).
:rtype: dict
"""
request_ref = 'https://www.googleapis.com/oauth2/v4/token'
auth_url_values = parse_qs(url[url.index('?') + 1:])
data = {
'client_id': self.client_secret['client_id'],
'client_secret': self.client_secret['client_secret'],
'code': auth_url_values['code'][0],
'grant_type': 'authorization_code',
'redirect_uri': self.client_secret['redirect_uris'][0],
}
headers = {"content-type": "application/x-www-form-urlencoded; charset=UTF-8"}
request_object = self.requests.post(request_ref, headers=headers, data=data)
raise_detailed_error(request_object)
return {
'type': 'id_token',
'value': request_object.json()['id_token'],
}
[docs] def update_profile(self, id_token, display_name=None, photo_url=None, delete_attribute=None):
""" Update a user's profile (display name / photo URL).
| For more details:
| `Firebase Auth REST API | section-update-profile`_
.. _Firebase Auth REST API | section-update-profile: https://firebase.google.com/docs/reference/rest/auth#section-update-profile
:type id_token: str
:param id_token: A Firebase Auth ID token for the user.
:type display_name: str or None
:param display_name: User's new display name.
:type photo_url: None or str
:param photo_url: User's new photo url.
:type delete_attribute: list[str] or None
:param delete_attribute: List of attributes to delete,
"DISPLAY_NAME" or "PHOTO_URL". This will nullify these
values.
:return: UserInfo and Firebase Auth Tokens.
:rtype: dict
"""
request_ref = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/setAccountInfo?key={0}".format(self.api_key)
headers = {"content-type": "application/json; charset=UTF-8"}
data = json.dumps({"idToken": id_token, "displayName": display_name, "photoURL": photo_url, "deleteAttribute": delete_attribute, "returnSecureToken": True})
request_object = self.requests.post(request_ref, headers=headers, data=data)
raise_detailed_error(request_object)
return request_object.json()
def _load_client_secret(secret):
""" Load social providers' client secret from file if file path
provided.
This function also restructures the dict object to make it
compatible for usage.
:type secret: str or dict
:param secret: File path to or the dict object from social client
secret file.
:return: social client secret
:rtype: dict
"""
if type(secret) is str:
with open(secret) as file:
secret = json.load(file)
# Google client secrets are stored within 'web' key
# We will remove the key, and replace it with the dict type value of it
secret = secret['web']
return secret