diff --git a/firebase_admin/_auth_utils.py b/firebase_admin/_auth_utils.py index b6788355c..d9b8c66e0 100644 --- a/firebase_admin/_auth_utils.py +++ b/firebase_admin/_auth_utils.py @@ -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([ @@ -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) diff --git a/firebase_admin/_http_client.py b/firebase_admin/_http_client.py index 73028f833..eb8c4027a 100644 --- a/firebase_admin/_http_client.py +++ b/firebase_admin/_http_client.py @@ -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) diff --git a/firebase_admin/_token_gen.py b/firebase_admin/_token_gen.py index 0fcb1d0c7..0ea34f77c 100644 --- a/firebase_admin/_token_gen.py +++ b/firebase_admin/_token_gen.py @@ -29,6 +29,7 @@ import google.oauth2.service_account from firebase_admin import exceptions +from firebase_admin import _auth_utils # ID token constants @@ -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.""" @@ -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): diff --git a/firebase_admin/auth.py b/firebase_admin/auth.py index 5e168b2fe..cd9a882e6 100644 --- a/firebase_admin/auth.py +++ b/firebase_admin/auth.py @@ -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 @@ -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 @@ -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): diff --git a/integration/test_auth.py b/integration/test_auth.py index 53577b827..a2905d881 100644 --- a/integration/test_auth.py +++ b/integration/test_auth.py @@ -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') diff --git a/tests/test_token_gen.py b/tests/test_token_gen.py index 3a25640aa..baf8d9515 100644 --- a/tests/test_token_gen.py +++ b/tests/test_token_gen.py @@ -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)