Skip to content

Raising FirebaseError from create_session_cookie() API #306

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Jul 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions firebase_admin/_auth_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
import six
from six.moves import urllib

from firebase_admin import exceptions
from firebase_admin import _utils


MAX_CLAIMS_PAYLOAD_SIZE = 1000
RESERVED_CLAIMS = set([
Expand Down Expand Up @@ -188,3 +191,71 @@ def validate_action_type(action_type):
raise ValueError('Invalid action type provided action_type: {0}. \
Valid values are {1}'.format(action_type, ', '.join(VALID_EMAIL_ACTION_TYPES)))
return action_type


class InvalidIdTokenError(exceptions.InvalidArgumentError):
"""The provided ID token is not a valid Firebase ID token."""

default_message = 'The provided ID token is invalid'

def __init__(self, message, cause, http_response=None):
exceptions.InvalidArgumentError.__init__(self, message, cause, http_response)


class UnexpectedResponseError(exceptions.UnknownError):
"""Backend service responded with an unexpected or malformed response."""

def __init__(self, message, cause=None, http_response=None):
exceptions.UnknownError.__init__(self, message, cause, http_response)


_CODE_TO_EXC_TYPE = {
'INVALID_ID_TOKEN': InvalidIdTokenError,
}


def handle_auth_backend_error(error):
"""Converts a requests error received from the Firebase Auth service into a FirebaseError."""
if error.response is None:
raise _utils.handle_requests_error(error)

code, custom_message = _parse_error_body(error.response)
if not code:
msg = 'Unexpected error response: {0}'.format(error.response.content.decode())
raise _utils.handle_requests_error(error, message=msg)

exc_type = _CODE_TO_EXC_TYPE.get(code)
msg = _build_error_message(code, exc_type, custom_message)
if not exc_type:
return _utils.handle_requests_error(error, message=msg)

return exc_type(msg, cause=error, http_response=error.response)


def _parse_error_body(response):
"""Parses the given error response to extract Auth error code and message."""
error_dict = {}
try:
parsed_body = response.json()
if isinstance(parsed_body, dict):
error_dict = parsed_body.get('error', {})
except ValueError:
pass

# Auth error response format: {"error": {"message": "AUTH_ERROR_CODE: Optional text"}}
code = error_dict.get('message')
custom_message = None
if code:
separator = code.find(':')
if separator != -1:
custom_message = code[separator + 1:].strip()
code = code[:separator]

return code, custom_message


def _build_error_message(code, exc_type, custom_message):
default_message = exc_type.default_message if (
exc_type and hasattr(exc_type, 'default_message')) else 'Error while calling Auth service'
ext = ' {0}'.format(custom_message) if custom_message else ''
return '{0} ({1}).{2}'.format(default_message, code, ext)
4 changes: 4 additions & 0 deletions firebase_admin/_http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ def headers(self, method, url, **kwargs):
resp = self.request(method, url, **kwargs)
return resp.headers

def body_and_response(self, method, url, **kwargs):
resp = self.request(method, url, **kwargs)
return self.parse_body(resp), resp

def body(self, method, url, **kwargs):
resp = self.request(method, url, **kwargs)
return self.parse_body(resp)
Expand Down
32 changes: 8 additions & 24 deletions firebase_admin/_token_gen.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import google.oauth2.service_account

from firebase_admin import exceptions
from firebase_admin import _auth_utils


# ID token constants
Expand All @@ -53,18 +54,6 @@
METADATA_SERVICE_URL = ('http://metadata/computeMetadata/v1/instance/service-accounts/'
'default/email')

# Error codes
COOKIE_CREATE_ERROR = 'COOKIE_CREATE_ERROR'


class ApiCallError(Exception):
"""Represents an Exception encountered while invoking the ID toolkit API."""

def __init__(self, code, message, error=None):
Exception.__init__(self, message)
self.code = code
self.detail = error


class _SigningProvider(object):
"""Stores a reference to a google.auth.crypto.Signer."""
Expand Down Expand Up @@ -207,20 +196,15 @@ def create_session_cookie(self, id_token, expires_in):
'validDuration': expires_in,
}
try:
response = self.client.body('post', ':createSessionCookie', json=payload)
body, http_resp = self.client.body_and_response(
'post', ':createSessionCookie', json=payload)
except requests.exceptions.RequestException as error:
self._handle_http_error(COOKIE_CREATE_ERROR, 'Failed to create session cookie', error)
else:
if not response or not response.get('sessionCookie'):
raise ApiCallError(COOKIE_CREATE_ERROR, 'Failed to create session cookie.')
return response.get('sessionCookie')

def _handle_http_error(self, code, msg, error):
if error.response is not None:
msg += '\nServer response: {0}'.format(error.response.content.decode())
raise _auth_utils.handle_auth_backend_error(error)
else:
msg += '\nReason: {0}'.format(error)
raise ApiCallError(code, msg, error)
if not body or not body.get('sessionCookie'):
raise _auth_utils.UnexpectedResponseError(
'Failed to create session cookie.', http_response=http_resp)
return body.get('sessionCookie')


class TokenVerifier(object):
Expand Down
10 changes: 5 additions & 5 deletions firebase_admin/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import time

import firebase_admin
from firebase_admin import _auth_utils
from firebase_admin import _http_client
from firebase_admin import _token_gen
from firebase_admin import _user_import
Expand Down Expand Up @@ -76,7 +77,9 @@
ListUsersPage = _user_mgt.ListUsersPage
UserImportHash = _user_import.UserImportHash
ImportUserRecord = _user_import.ImportUserRecord
InvalidIdTokenError = _auth_utils.InvalidIdTokenError
TokenSignError = _token_gen.TokenSignError
UnexpectedResponseError = _auth_utils.UnexpectedResponseError
UserImportResult = _user_import.UserImportResult
UserInfo = _user_mgt.UserInfo
UserMetadata = _user_mgt.UserMetadata
Expand Down Expand Up @@ -169,13 +172,10 @@ def create_session_cookie(id_token, expires_in, app=None):

Raises:
ValueError: If input parameters are invalid.
AuthError: If an error occurs while creating the cookie.
FirebaseError: If an error occurs while creating the cookie.
"""
token_generator = _get_auth_service(app).token_generator
try:
return token_generator.create_session_cookie(id_token, expires_in)
except _token_gen.ApiCallError as error:
raise AuthError(error.code, str(error), error.detail)
return token_generator.create_session_cookie(id_token, expires_in)


def verify_session_cookie(session_cookie, check_revoked=False, app=None):
Expand Down
5 changes: 5 additions & 0 deletions integration/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@ def test_session_cookies(api_key):
estimated_exp = int(time.time() + expires_in.total_seconds())
assert abs(claims['exp'] - estimated_exp) < 5

def test_session_cookie_error():
expires_in = datetime.timedelta(days=1)
with pytest.raises(auth.InvalidIdTokenError):
auth.create_session_cookie('not.a.token', expires_in=expires_in)

def test_get_non_existing_user():
with pytest.raises(auth.AuthError) as excinfo:
auth.get_user('non.existing')
Expand Down
33 changes: 27 additions & 6 deletions tests/test_token_gen.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,17 +301,38 @@ def test_valid_args(self, user_mgt_app, expires_in):
assert request == {'idToken' : 'id_token', 'validDuration': 3600}

def test_error(self, user_mgt_app):
_instrument_user_manager(user_mgt_app, 500, '{"error":"test"}')
with pytest.raises(auth.AuthError) as excinfo:
_instrument_user_manager(user_mgt_app, 500, '{"error":{"message": "INVALID_ID_TOKEN"}}')
with pytest.raises(auth.InvalidIdTokenError) as excinfo:
auth.create_session_cookie('id_token', expires_in=3600, app=user_mgt_app)
assert excinfo.value.code == exceptions.INVALID_ARGUMENT
assert str(excinfo.value) == 'The provided ID token is invalid (INVALID_ID_TOKEN).'

def test_error_with_details(self, user_mgt_app):
_instrument_user_manager(
user_mgt_app, 500, '{"error":{"message": "INVALID_ID_TOKEN: More details."}}')
with pytest.raises(auth.InvalidIdTokenError) as excinfo:
auth.create_session_cookie('id_token', expires_in=3600, app=user_mgt_app)
assert excinfo.value.code == exceptions.INVALID_ARGUMENT
expected = 'The provided ID token is invalid (INVALID_ID_TOKEN). More details.'
assert str(excinfo.value) == expected

def test_unexpected_error_code(self, user_mgt_app):
_instrument_user_manager(user_mgt_app, 500, '{"error":{"message": "SOMETHING_UNUSUAL"}}')
with pytest.raises(exceptions.InternalError) as excinfo:
auth.create_session_cookie('id_token', expires_in=3600, app=user_mgt_app)
assert str(excinfo.value) == 'Error while calling Auth service (SOMETHING_UNUSUAL).'

def test_unexpected_error_response(self, user_mgt_app):
_instrument_user_manager(user_mgt_app, 500, '{}')
with pytest.raises(exceptions.InternalError) as excinfo:
auth.create_session_cookie('id_token', expires_in=3600, app=user_mgt_app)
assert excinfo.value.code == _token_gen.COOKIE_CREATE_ERROR
assert '{"error":"test"}' in str(excinfo.value)
assert str(excinfo.value) == 'Unexpected error response: {}'

def test_unexpected_response(self, user_mgt_app):
_instrument_user_manager(user_mgt_app, 200, '{}')
with pytest.raises(auth.AuthError) as excinfo:
with pytest.raises(auth.UnexpectedResponseError) as excinfo:
auth.create_session_cookie('id_token', expires_in=3600, app=user_mgt_app)
assert excinfo.value.code == _token_gen.COOKIE_CREATE_ERROR
assert excinfo.value.code == exceptions.UNKNOWN
assert 'Failed to create session cookie' in str(excinfo.value)


Expand Down