├── .gitignore ├── LICENSE ├── README.md ├── drf_firebase ├── __init__.py ├── apps.py └── authentication.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/linux,macos,django,python,windows 2 | # Edit at https://www.gitignore.io/?templates=linux,macos,django,python,windows 3 | 4 | ### Django ### 5 | *.log 6 | *.pot 7 | *.pyc 8 | __pycache__/ 9 | local_settings.py 10 | db.sqlite3 11 | media 12 | 13 | # If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/ 14 | # in your Git repository. Update and uncomment the following line accordingly. 15 | # /staticfiles/ 16 | 17 | ### Django.Python Stack ### 18 | # Byte-compiled / optimized / DLL files 19 | *.py[cod] 20 | *$py.class 21 | 22 | # C extensions 23 | *.so 24 | 25 | # Distribution / packaging 26 | .Python 27 | build/ 28 | develop-eggs/ 29 | dist/ 30 | downloads/ 31 | eggs/ 32 | .eggs/ 33 | lib/ 34 | lib64/ 35 | parts/ 36 | sdist/ 37 | var/ 38 | wheels/ 39 | pip-wheel-metadata/ 40 | share/python-wheels/ 41 | *.egg-info/ 42 | .installed.cfg 43 | *.egg 44 | MANIFEST 45 | 46 | # PyInstaller 47 | # Usually these files are written by a python script from a template 48 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 49 | *.manifest 50 | *.spec 51 | 52 | # Installer logs 53 | pip-log.txt 54 | pip-delete-this-directory.txt 55 | 56 | # Unit test / coverage reports 57 | htmlcov/ 58 | .tox/ 59 | .nox/ 60 | .coverage 61 | .coverage.* 62 | .cache 63 | nosetests.xml 64 | coverage.xml 65 | *.cover 66 | .hypothesis/ 67 | .pytest_cache/ 68 | 69 | # Translations 70 | *.mo 71 | 72 | # Django stuff: 73 | 74 | # Flask stuff: 75 | instance/ 76 | .webassets-cache 77 | 78 | # Scrapy stuff: 79 | .scrapy 80 | 81 | # Sphinx documentation 82 | docs/_build/ 83 | 84 | # PyBuilder 85 | target/ 86 | 87 | # Jupyter Notebook 88 | .ipynb_checkpoints 89 | 90 | # IPython 91 | profile_default/ 92 | ipython_config.py 93 | 94 | # pyenv 95 | .python-version 96 | 97 | # pipenv 98 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 99 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 100 | # having no cross-platform support, pipenv may install dependencies that don’t work, or not 101 | # install all needed dependencies. 102 | #Pipfile.lock 103 | 104 | # celery beat schedule file 105 | celerybeat-schedule 106 | 107 | # SageMath parsed files 108 | *.sage.py 109 | 110 | # Environments 111 | .env 112 | .venv 113 | env/ 114 | venv/ 115 | ENV/ 116 | env.bak/ 117 | venv.bak/ 118 | 119 | # Spyder project settings 120 | .spyderproject 121 | .spyproject 122 | 123 | # Rope project settings 124 | .ropeproject 125 | 126 | # mkdocs documentation 127 | /site 128 | 129 | # mypy 130 | .mypy_cache/ 131 | .dmypy.json 132 | dmypy.json 133 | 134 | # Pyre type checker 135 | .pyre/ 136 | 137 | ### Linux ### 138 | *~ 139 | 140 | # temporary files which can be created if a process still has a handle open of a deleted file 141 | .fuse_hidden* 142 | 143 | # KDE directory preferences 144 | .directory 145 | 146 | # Linux trash folder which might appear on any partition or disk 147 | .Trash-* 148 | 149 | # .nfs files are created when an open file is removed but is still being accessed 150 | .nfs* 151 | 152 | ### macOS ### 153 | # General 154 | .DS_Store 155 | .AppleDouble 156 | .LSOverride 157 | 158 | # Icon must end with two \r 159 | Icon 160 | 161 | # Thumbnails 162 | ._* 163 | 164 | # Files that might appear in the root of a volume 165 | .DocumentRevisions-V100 166 | .fseventsd 167 | .Spotlight-V100 168 | .TemporaryItems 169 | .Trashes 170 | .VolumeIcon.icns 171 | .com.apple.timemachine.donotpresent 172 | 173 | # Directories potentially created on remote AFP share 174 | .AppleDB 175 | .AppleDesktop 176 | Network Trash Folder 177 | Temporary Items 178 | .apdisk 179 | 180 | ### Python ### 181 | # Byte-compiled / optimized / DLL files 182 | 183 | # C extensions 184 | 185 | # Distribution / packaging 186 | 187 | # PyInstaller 188 | # Usually these files are written by a python script from a template 189 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 190 | 191 | # Installer logs 192 | 193 | # Unit test / coverage reports 194 | 195 | # Translations 196 | 197 | # Django stuff: 198 | 199 | # Flask stuff: 200 | 201 | # Scrapy stuff: 202 | 203 | # Sphinx documentation 204 | 205 | # PyBuilder 206 | 207 | # Jupyter Notebook 208 | 209 | # IPython 210 | 211 | # pyenv 212 | 213 | # pipenv 214 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 215 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 216 | # having no cross-platform support, pipenv may install dependencies that don’t work, or not 217 | # install all needed dependencies. 218 | 219 | # celery beat schedule file 220 | 221 | # SageMath parsed files 222 | 223 | # Environments 224 | 225 | # Spyder project settings 226 | 227 | # Rope project settings 228 | 229 | # mkdocs documentation 230 | 231 | # mypy 232 | 233 | # Pyre type checker 234 | 235 | ### Windows ### 236 | # Windows thumbnail cache files 237 | Thumbs.db 238 | ehthumbs.db 239 | ehthumbs_vista.db 240 | 241 | # Dump file 242 | *.stackdump 243 | 244 | # Folder config file 245 | [Dd]esktop.ini 246 | 247 | # Recycle Bin used on file shares 248 | $RECYCLE.BIN/ 249 | 250 | # Windows Installer files 251 | *.cab 252 | *.msi 253 | *.msix 254 | *.msm 255 | *.msp 256 | 257 | # Windows shortcuts 258 | *.lnk 259 | 260 | # End of https://www.gitignore.io/api/linux,macos,django,python,windows 261 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Jonathan Ehwald 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django Rest Framework Firebase Authentication 2 | 3 | ![PyPI](https://img.shields.io/pypi/v/drf-firebase-authentication) 4 | ![PyPI - License](https://img.shields.io/pypi/l/drf-firebase-authentication) 5 | 6 | This package provides a base Firebase Authentication backend class for the Django rest framework. Two key methods are not implemented for more flexebility. Let's [keep it simple, stupid](https://en.wikipedia.org/wiki/KISS_principle). 7 | 8 | ## Requirements 9 | 10 | - Python 2.7 or 3.4+ 11 | - [Django](https://github.com/django/django) (version 1.11+) 12 | - [Django Rest Framework](https://github.com/encode/django-rest-framework) 13 | - [Firebase Admin Python](https://github.com/firebase/firebase-admin-python) 14 | 15 | ## Installation 16 | 17 | ```pip install drf-firebase-authentication``` 18 | 19 | ## Usage 20 | 21 | #### 1. Subclass `BaseFirebaseAuthentication` and implement its template methods: 22 | 23 | Decide by yourself how you want to associate Firebase users with local django users by implementing the `.get_django_user()` method accordingly. 24 | 25 | Put your code into a `authentication.py` file inside an app dedicated to your projects REST Api. 26 | 27 | ```python 28 | from drf_firebase.authentication import BaseFirebaseAuthentication 29 | from firebase_admin import credentials, initialize_app 30 | from django.contrib.auth import get_user_model 31 | 32 | firebase_creds = credentials.Certificate('path/to/firebase/credentials.json') 33 | firebase_app = initialize_app(firebase_creds) 34 | 35 | class FirebaseAuthentication(BaseFirebaseAuthentication): 36 | """ 37 | Example implementation of a DRF Firebase Authentication backend class 38 | """ 39 | def get_firebase_app(self): 40 | return firebase_app 41 | 42 | def get_django_user(self, firebase_user_record): 43 | return get_user_model().objects.get_or_create( 44 | username=firebase_user_record.uid, 45 | )[0] 46 | ``` 47 | 48 | #### 2. Add the just created Firebase authentication backend to your `settings.py`: 49 | 50 | Replace `YOUR_RESTAPI_APP` with the app you put your `authentication.py` file in. 51 | 52 | ```python 53 | REST_FRAMEWORK = { 54 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 55 | 'rest_framework.authentication.SessionAuthentication', # default 56 | 'rest_framework.authentication.BasicAuthentication', # default 57 | 'YOUR_RESTAPI_APP.authentication.FirebaseAuthentication', 58 | ), 59 | } 60 | ``` 61 | -------------------------------------------------------------------------------- /drf_firebase/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '1.0.1' 2 | 3 | default_app_config = 'drf_firebase.apps.DRFFirebaseAuthenticationConfig' 4 | -------------------------------------------------------------------------------- /drf_firebase/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | class DRFFirebaseAuthenticationConfig(AppConfig): 4 | name = 'drf_firebase_authentication' -------------------------------------------------------------------------------- /drf_firebase/authentication.py: -------------------------------------------------------------------------------- 1 | from rest_framework import authentication 2 | from rest_framework.exceptions import AuthenticationFailed 3 | from django.utils.translation import gettext_lazy as _ 4 | from firebase_admin import auth as firebase_auth 5 | 6 | 7 | class BaseFirebaseAuthentication(authentication.BaseAuthentication): 8 | """ 9 | Firebase Authentication based django restframework authentication class. 10 | 11 | Clients should authenticate by passing a Firebase ID Token in the 12 | "Authorization" HTTP header, preprended with the string " " where 13 | is this classes `keyword` string property. For example: 14 | 15 | Authorization: FirebaseToken xxxxx.yyyyy.zzzzz 16 | """ 17 | keyword = 'FirebaseToken' 18 | check_revoked = False 19 | 20 | def authenticate(self, request): 21 | auth = authentication.get_authorization_header(request).split() 22 | 23 | if not auth or auth[0].lower() != self.keyword.lower().encode(): 24 | return None 25 | 26 | if len(auth) == 1: 27 | msg = _('Invalid token header. No credentials provided.') 28 | raise AuthenticationFailed(msg) 29 | if len(auth) > 2: 30 | msg = _('Invalid token header. Token string should not contain spaces.') 31 | raise AuthenticationFailed(msg) 32 | 33 | try: 34 | firebase_token = auth[1].decode() 35 | except UnicodeError: 36 | msg = _('Invalid token header. Token string should not contain invalid characters.') 37 | raise AuthenticationFailed(msg) 38 | 39 | return self.authenticate_credentials(firebase_token) 40 | 41 | def authenticate_credentials(self, firebase_token): 42 | try: 43 | decoded_token = firebase_auth.verify_id_token( 44 | firebase_token, 45 | app=self.get_firebase_app(), 46 | check_revoked=self.check_revoked, 47 | ) 48 | except (ValueError, firebase_auth.InvalidIdTokenError): 49 | # Token was either not a string or empty or not an valid Firebase ID token 50 | msg = _('The Firebase token was invalid.') 51 | raise AuthenticationFailed(msg) 52 | except firebase_auth.ExpiredIdTokenError: 53 | msg = _('The Firebase token has expired.') 54 | raise AuthenticationFailed(msg) 55 | except firebase_auth.RevokedIdTokenError: 56 | msg = _('The Firebase token has been revoked.') 57 | raise AuthenticationFailed(msg) 58 | except firebase_auth.CertificateFetchError: 59 | msg = _('Temporarily unable to verify the ID token.') 60 | raise AuthenticationFailed(msg) 61 | 62 | firebase_uid = decoded_token['uid'] 63 | firebase_user_record = firebase_auth.get_user( 64 | firebase_uid, 65 | app=self.get_firebase_app(), 66 | ) 67 | 68 | # This template method must be implemented in a subclass. 69 | user = self.get_django_user(firebase_user_record) 70 | 71 | if user is None: 72 | msg = _('No matching local user found.') 73 | raise AuthenticationFailed(msg) 74 | 75 | return (user, firebase_token) 76 | 77 | def authenticate_header(self, request): 78 | """ 79 | Returns a string that will be used as the value of the WWW-Authenticate 80 | header in a HTTP 401 Unauthorized response. 81 | """ 82 | return self.keyword 83 | 84 | @classmethod 85 | def get_firebase_app(cls): 86 | """ 87 | This method must be implemented in a subclass of this class. It must 88 | return a valid Firebase App instance. 89 | 90 | Firebase App docs: 91 | https://firebase.google.com/docs/reference/admin/python/firebase_admin#app 92 | """ 93 | msg = 'Implement this method in a subclass.' 94 | raise NotImplementedError(msg) 95 | 96 | @classmethod 97 | def get_django_user(cls, firebase_user_record): 98 | """ 99 | Returns a django user associated with the requesting firebase user. 100 | 101 | As we don't know how you intent on associating a firebase user with 102 | your local django users, this method must be implemented in a subclass. 103 | 104 | This method takes a firebase UserRecord object as parameter. You may 105 | use its properties to find or create a local django user. 106 | 107 | Return None in case you want the authentication process to fail. 108 | eg. when the requesting users email address is not verified. 109 | 110 | Firebase UserRecord ocs: 111 | https://firebase.google.com/docs/reference/admin/python/firebase_admin.auth#userrecord 112 | """ 113 | msg = 'Implement this method in a subclass.' 114 | raise NotImplementedError(msg) 115 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import find_packages, setup 3 | from drf_firebase import __version__ 4 | 5 | with open(os.path.join(os.path.dirname(__file__), 'README.md')) as readme: 6 | README = readme.read() 7 | 8 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 9 | 10 | setup( 11 | name='drf-firebase-authentication', 12 | version=__version__, 13 | packages=find_packages(), 14 | include_package_data=True, 15 | license='MIT License', 16 | description='A flexible Django Rest Framework Firebase authentication class', 17 | long_description=README, 18 | long_description_content_type='text/markdown', 19 | url='https://github.com/DoctorJohn/drf-firebase-authentication', 20 | author='Jonathan Ehwald', 21 | author_email='pypi@ehwald.info', 22 | install_requires=[ 23 | 'firebase-admin~=3.0.0', 24 | 'djangorestframework>=3.1', 25 | 'Django>=1.11', 26 | ], 27 | classifiers=[ 28 | 'Environment :: Web Environment', 29 | 'Framework :: Django', 30 | 'Framework :: Django :: 1.11', 31 | 'Framework :: Django :: 2.0', 32 | 'Framework :: Django :: 2.1', 33 | 'Framework :: Django :: 2.2', 34 | 'Intended Audience :: Developers', 35 | 'License :: OSI Approved :: MIT License', 36 | 'Operating System :: OS Independent', 37 | 'Programming Language :: Python', 38 | 'Programming Language :: Python :: 3', 39 | 'Programming Language :: Python :: 3.3', 40 | 'Programming Language :: Python :: 3.4', 41 | 'Programming Language :: Python :: 3.5', 42 | 'Programming Language :: Python :: 3.6', 43 | 'Programming Language :: Python :: 3.7', 44 | 'Topic :: Internet :: WWW/HTTP', 45 | ], 46 | ) 47 | --------------------------------------------------------------------------------