diff --git a/firebase/firebase.py b/firebase/firebase.py index 2e55865..96be331 100644 --- a/firebase/firebase.py +++ b/firebase/firebase.py @@ -1,12 +1,12 @@ try: import urlparse except ImportError: - #py3k + # py3k from urllib import parse as urlparse import json +from firebase_token_generator import create_token -from .firebase_token_generator import FirebaseTokenGenerator from .decorators import http_connection from .async import process_pool @@ -93,8 +93,7 @@ def make_post_request(url, data, params, headers, connection): response => {u'name': u'-Inw6zol_2f5ThHwVcSe'} or {'error': 'Permission denied.'} """ timeout = getattr(connection, 'timeout') - response = connection.post(url, data=data, params=params, headers=headers, - timeout=timeout) + response = connection.post(url, data=data, params=params, headers=headers, timeout=timeout) if response.ok or response.status_code == 403: return response.json() if response.content else None else: @@ -122,8 +121,7 @@ def make_patch_request(url, data, params, headers, connection): response => {'Ozgur Vatansever'} or {'error': 'Permission denied.'} """ timeout = getattr(connection, 'timeout') - response = connection.patch(url, data=data, params=params, headers=headers, - timeout=timeout) + response = connection.patch(url, data=data, params=params, headers=headers, timeout=timeout) if response.ok or response.status_code == 403: return response.json() if response.content else None else: @@ -162,11 +160,16 @@ class FirebaseUser(object): Class that wraps the credentials of the authenticated user. Think of this as a container that holds authentication related data. """ - def __init__(self, email, firebase_auth_token, provider, id=None): + + def __init__(self, email, firebase_auth_token, provider, uid=None): self.email = email self.firebase_auth_token = firebase_auth_token self.provider = provider - self.id = id + self.uid = uid + + def __repr__(self): + return '<%s email="%s" uid="%s" provider="%s" firebase_auth_token="%s">' % ( + self.__class__.__name__, self.email, self.uid, self.provider, self.firebase_auth_token) class FirebaseAuthentication(object): @@ -177,21 +180,26 @@ class does not trigger a connection, simply fakes the auth action. In addition, the provided email and password information is totally useless and they never appear in the ``auth`` variable at the server. """ - def __init__(self, secret, email, debug=False, admin=False, extra=None): - self.authenticator = FirebaseTokenGenerator(secret, debug, admin) + + def __init__(self, secret, email, auth_payload=None): + assert secret, 'Your Firebase SECRET is not valid' + self.secret = secret self.email = email self.provider = 'password' - self.extra = (extra or {}).copy() - self.extra.update({'debug': debug, 'admin': admin, - 'email': self.email, 'provider': self.provider}) + self.auth_payload = (auth_payload or {}).copy() - def get_user(self): + def get_user(self, expires=None, not_before=None, admin=False, debug=False, simulate=False): """ Method that gets the authenticated user. The returning user has the token, email and the provider data. """ - token = self.authenticator.create_token(self.extra) - user_id = self.extra.get('id') + options = {'admin': admin, 'debug': debug, 'simulate': simulate} + if expires is not None: + options['expires'] = expires + if not_before is not None: + options['notBefore'] = not_before + token = create_token(self.secret, self.auth_payload, options) + user_id = self.auth_payload.get('uid') return FirebaseUser(self.email, token, self.provider, user_id) @@ -240,7 +248,7 @@ def _build_endpoint_url(self, url, name=None): full_url => 'http://firebase.localhost/users/1.json' """ if not url.endswith(self.URL_SEPERATOR): - url = url + self.URL_SEPERATOR + url += self.URL_SEPERATOR if name is None: name = '' return '%s%s%s' % (urlparse.urljoin(self.dsn, url), name, @@ -259,14 +267,15 @@ def _authenticate(self, params, headers): if self.authentication: user = self.authentication.get_user() params.update({'auth': user.firebase_auth_token}) - headers.update(self.authentication.authenticator.HEADERS) + # headers.update(self.authentication.authenticator.HEADERS) @http_connection(60) def get(self, url, name, params=None, headers=None, connection=None): """ Synchronous GET request. """ - if name is None: name = '' + if name is None: + name = '' params = params or {} headers = headers or {} endpoint = self._build_endpoint_url(url, name) @@ -277,13 +286,14 @@ def get_async(self, url, name, callback=None, params=None, headers=None): """ Asynchronous GET request with the process pool. """ - if name is None: name = '' + if name is None: + name = '' params = params or {} headers = headers or {} endpoint = self._build_endpoint_url(url, name) self._authenticate(params, headers) process_pool.apply_async(make_get_request, - args=(endpoint, params, headers), callback=callback) + args=(endpoint, params, headers), callback=callback) @http_connection(60) def put(self, url, name, data, params=None, headers=None, connection=None): @@ -305,7 +315,8 @@ def put_async(self, url, name, data, callback=None, params=None, headers=None): """ Asynchronous PUT request with the process pool. """ - if name is None: name = '' + if name is None: + name = '' params = params or {} headers = headers or {} endpoint = self._build_endpoint_url(url, name) @@ -372,7 +383,8 @@ def delete(self, url, name, params=None, headers=None, connection=None): """ Synchronous DELETE request. ``data`` must be a JSONable value. """ - if not name: name = '' + if not name: + name = '' params = params or {} headers = headers or {} endpoint = self._build_endpoint_url(url, name) @@ -383,10 +395,11 @@ def delete_async(self, url, name, callback=None, params=None, headers=None): """ Asynchronous DELETE request with the process pool. """ - if not name: name = '' + if not name: + name = '' params = params or {} headers = headers or {} endpoint = self._build_endpoint_url(url, name) self._authenticate(params, headers) process_pool.apply_async(make_delete_request, - args=(endpoint, params, headers), callback=callback) + args=(endpoint, params, headers), callback=callback) diff --git a/firebase/firebase_token_generator.py b/firebase/firebase_token_generator.py deleted file mode 100644 index 2a3d869..0000000 --- a/firebase/firebase_token_generator.py +++ /dev/null @@ -1,116 +0,0 @@ -############################################################################## -# THE ENTIRE CODE HAS BEEN TAKEN FROM THE OFFICIAL FIREBASE GITHUB # -# REPOSITORY NAMED `firebase-token-generator-python` WITH SLIGHT # -# MODIFICATIONS. # -# # -# FOR MORE INFORMATION, PLEASE TAKE A LOOK AT THE ACTUAL REPOSITORY: # -# - https://github.com/firebase/firebase-token-generator-python # -############################################################################## -import base64 -import hashlib -import hmac -import json -import time - -__all__ = ['FirebaseTokenGenerator'] - - -class FirebaseTokenGenerator(object): - TOKEN_VERSION = 0 - TOKEN_SEP = '.' - CLAIMS_MAP = { - 'expires': 'exp', - 'notBefore': 'nbf', - 'admin': 'admin', - 'debug': 'debug', - 'simulate': 'simulate' - } - HEADERS = {'typ': 'JWT', 'alg': 'HS256'} - - def __init__(self, secret, debug=False, admin=False): - assert secret, 'Your Firebase SECRET is not valid' - self.secret = secret - self.admin = admin - self.debug = debug - - def create_token(self, data, options=None): - """ - Generates a secure authentication token. - - Our token format follows the JSON Web Token (JWT) standard: - header.claims.signature - - Where: - 1) 'header' is a stringified, base64-encoded JSON object containing version and algorithm information. - 2) 'claims' is a stringified, base64-encoded JSON object containing a set of claims: - Library-generated claims: - 'iat' -> The issued at time in seconds since the epoch as a number - 'd' -> The arbitrary JSON object supplied by the user. - User-supplied claims (these are all optional): - 'exp' (optional) -> The expiration time of this token, as a number of seconds since the epoch. - 'nbf' (optional) -> The 'not before' time before which the token should be rejected (seconds since the epoch) - 'admin' (optional) -> If set to true, this client will bypass all security rules (use this to authenticate servers) - 'debug' (optional) -> 'set to true to make this client receive debug information about security rule execution. - 'simulate' (optional, internal-only for now) -> Set to true to neuter all API operations (listens / puts - will run security rules but not actually write or return data). - 3) A signature that proves the validity of this token (see: http://tools.ietf.org/html/draft-ietf-jose-json-web-signature-07) - - For base64-encoding we use URL-safe base64 encoding. This ensures that the entire token is URL-safe - and could, for instance, be placed as a query argument without any encoding (and this is what the JWT spec requires). - - Args: - data - a json serializable object of data to be included in the token - options - An optional dictionary of additional claims for the token. Possible keys include: - a) 'expires' -- A timestamp (as a number of seconds since the epoch) denoting a time after which - this token should no longer be valid. - b) 'notBefore' -- A timestamp (as a number of seconds since the epoch) denoting a time before - which this token should be rejected by the server. - c) 'admin' -- Set to true to bypass all security rules (use this for your trusted servers). - d) 'debug' -- Set to true to enable debug mode (so you can see the results of Rules API operations) - e) 'simulate' -- (internal-only for now) Set to true to neuter all API operations (listens / puts - will run security rules but not actually write or return data) - Returns: - A signed Firebase Authentication Token - Raises: - ValueError: if an invalid key is specified in options - """ - if not options: - options = {} - options.update({'admin': self.admin, 'debug': self.debug}) - claims = self._create_options_claims(options) - claims['v'] = self.TOKEN_VERSION - claims['iat'] = int(time.mktime(time.gmtime())) - claims['d'] = data - return self._encode_token(self.secret, claims) - - def _create_options_claims(self, opts): - claims = {} - for k in opts: - if k in self.CLAIMS_MAP: - claims[k] = opts[k] - else: - raise ValueError('Unrecognized Option: %s' % k) - return claims - - def _encode(self, bytes): - encoded = base64.urlsafe_b64encode(bytes) - return encoded.decode('utf-8').replace('=', '') - - def _encode_json(self, obj): - return self._encode(json.dumps(obj).encode("utf-8")) - - def _sign(self, secret, to_sign): - def portable_bytes(s): - try: - return bytes(s, 'utf-8') - except TypeError: - return bytes(s) - return self._encode(hmac.new(portable_bytes(secret), portable_bytes(to_sign), - hashlib.sha256).digest()) - - def _encode_token(self, secret, claims): - encoded_header = self._encode_json(self.HEADERS) - encoded_claims = self._encode_json(claims) - secure_bits = '%s%s%s' % (encoded_header, self.TOKEN_SEP, encoded_claims) - sig = self._sign(secret, secure_bits) - return '%s%s%s' % (secure_bits, self.TOKEN_SEP, sig) diff --git a/requirements.txt b/requirements.txt index 549d170..1a5cf12 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ requests>=1.1.0 +firebase-token-generator>=2.0.1 diff --git a/tests/firebase_test.py b/tests/firebase_test.py index 30e6068..7455a18 100644 --- a/tests/firebase_test.py +++ b/tests/firebase_test.py @@ -52,8 +52,9 @@ def setUp(self): self.SECRET = 'FAKE_FIREBASE_SECRET' self.DSN = 'https://firebase.localhost' self.EMAIL = 'python-firebase@firebase.com' + self.UID = '123' self.authentication = FirebaseAuthentication(self.SECRET, self.EMAIL, - None) + auth_payload={'uid': self.UID}) self.firebase = FirebaseApplication(self.DSN, self.authentication) def test_build_endpoint_url(self): @@ -95,4 +96,4 @@ def test_make_delete_request(self): connection = MockConnection(response) result = self.firebase.delete('url', 'snapshot', params={}, headers={}, connection=connection) - self.assertEqual(result, json.loads(response.content)) + self.assertEqual(result, json.loads(response.content)) \ No newline at end of file