Skip to content

Commit 99929ed

Browse files
authored
Raising FirebaseError from create_session_cookie() API (#306)
* Migrated FCM send APIs to the new error handling regime * Moved error parsing logic to _utils * Refactored OP error handling code * Fixing a broken test * Added utils for handling googleapiclient errors * Added tests for new error handling logic * Updated public API docs * Fixing test for python3 * Cleaning up the error code lookup code * Cleaning up the error parsing APIs * Cleaned up error parsing logic; Updated docs * Migrated the FCM IID APIs to the new error types * Migrated custom token API to new error types * Migrated create cookie API to new error types * Improved error message computation * Refactored the shared error handling code * Fixing lint errors * Renamed variable for clarity
1 parent b27216b commit 99929ed

File tree

6 files changed

+120
-35
lines changed

6 files changed

+120
-35
lines changed

firebase_admin/_auth_utils.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
import six
2121
from six.moves import urllib
2222

23+
from firebase_admin import exceptions
24+
from firebase_admin import _utils
25+
2326

2427
MAX_CLAIMS_PAYLOAD_SIZE = 1000
2528
RESERVED_CLAIMS = set([
@@ -188,3 +191,71 @@ def validate_action_type(action_type):
188191
raise ValueError('Invalid action type provided action_type: {0}. \
189192
Valid values are {1}'.format(action_type, ', '.join(VALID_EMAIL_ACTION_TYPES)))
190193
return action_type
194+
195+
196+
class InvalidIdTokenError(exceptions.InvalidArgumentError):
197+
"""The provided ID token is not a valid Firebase ID token."""
198+
199+
default_message = 'The provided ID token is invalid'
200+
201+
def __init__(self, message, cause, http_response=None):
202+
exceptions.InvalidArgumentError.__init__(self, message, cause, http_response)
203+
204+
205+
class UnexpectedResponseError(exceptions.UnknownError):
206+
"""Backend service responded with an unexpected or malformed response."""
207+
208+
def __init__(self, message, cause=None, http_response=None):
209+
exceptions.UnknownError.__init__(self, message, cause, http_response)
210+
211+
212+
_CODE_TO_EXC_TYPE = {
213+
'INVALID_ID_TOKEN': InvalidIdTokenError,
214+
}
215+
216+
217+
def handle_auth_backend_error(error):
218+
"""Converts a requests error received from the Firebase Auth service into a FirebaseError."""
219+
if error.response is None:
220+
raise _utils.handle_requests_error(error)
221+
222+
code, custom_message = _parse_error_body(error.response)
223+
if not code:
224+
msg = 'Unexpected error response: {0}'.format(error.response.content.decode())
225+
raise _utils.handle_requests_error(error, message=msg)
226+
227+
exc_type = _CODE_TO_EXC_TYPE.get(code)
228+
msg = _build_error_message(code, exc_type, custom_message)
229+
if not exc_type:
230+
return _utils.handle_requests_error(error, message=msg)
231+
232+
return exc_type(msg, cause=error, http_response=error.response)
233+
234+
235+
def _parse_error_body(response):
236+
"""Parses the given error response to extract Auth error code and message."""
237+
error_dict = {}
238+
try:
239+
parsed_body = response.json()
240+
if isinstance(parsed_body, dict):
241+
error_dict = parsed_body.get('error', {})
242+
except ValueError:
243+
pass
244+
245+
# Auth error response format: {"error": {"message": "AUTH_ERROR_CODE: Optional text"}}
246+
code = error_dict.get('message')
247+
custom_message = None
248+
if code:
249+
separator = code.find(':')
250+
if separator != -1:
251+
custom_message = code[separator + 1:].strip()
252+
code = code[:separator]
253+
254+
return code, custom_message
255+
256+
257+
def _build_error_message(code, exc_type, custom_message):
258+
default_message = exc_type.default_message if (
259+
exc_type and hasattr(exc_type, 'default_message')) else 'Error while calling Auth service'
260+
ext = ' {0}'.format(custom_message) if custom_message else ''
261+
return '{0} ({1}).{2}'.format(default_message, code, ext)

firebase_admin/_http_client.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ def headers(self, method, url, **kwargs):
109109
resp = self.request(method, url, **kwargs)
110110
return resp.headers
111111

112+
def body_and_response(self, method, url, **kwargs):
113+
resp = self.request(method, url, **kwargs)
114+
return self.parse_body(resp), resp
115+
112116
def body(self, method, url, **kwargs):
113117
resp = self.request(method, url, **kwargs)
114118
return self.parse_body(resp)

firebase_admin/_token_gen.py

Lines changed: 8 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import google.oauth2.service_account
3030

3131
from firebase_admin import exceptions
32+
from firebase_admin import _auth_utils
3233

3334

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

56-
# Error codes
57-
COOKIE_CREATE_ERROR = 'COOKIE_CREATE_ERROR'
58-
59-
60-
class ApiCallError(Exception):
61-
"""Represents an Exception encountered while invoking the ID toolkit API."""
62-
63-
def __init__(self, code, message, error=None):
64-
Exception.__init__(self, message)
65-
self.code = code
66-
self.detail = error
67-
6857

6958
class _SigningProvider(object):
7059
"""Stores a reference to a google.auth.crypto.Signer."""
@@ -207,20 +196,15 @@ def create_session_cookie(self, id_token, expires_in):
207196
'validDuration': expires_in,
208197
}
209198
try:
210-
response = self.client.body('post', ':createSessionCookie', json=payload)
199+
body, http_resp = self.client.body_and_response(
200+
'post', ':createSessionCookie', json=payload)
211201
except requests.exceptions.RequestException as error:
212-
self._handle_http_error(COOKIE_CREATE_ERROR, 'Failed to create session cookie', error)
213-
else:
214-
if not response or not response.get('sessionCookie'):
215-
raise ApiCallError(COOKIE_CREATE_ERROR, 'Failed to create session cookie.')
216-
return response.get('sessionCookie')
217-
218-
def _handle_http_error(self, code, msg, error):
219-
if error.response is not None:
220-
msg += '\nServer response: {0}'.format(error.response.content.decode())
202+
raise _auth_utils.handle_auth_backend_error(error)
221203
else:
222-
msg += '\nReason: {0}'.format(error)
223-
raise ApiCallError(code, msg, error)
204+
if not body or not body.get('sessionCookie'):
205+
raise _auth_utils.UnexpectedResponseError(
206+
'Failed to create session cookie.', http_response=http_resp)
207+
return body.get('sessionCookie')
224208

225209

226210
class TokenVerifier(object):

firebase_admin/auth.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import time
2323

2424
import firebase_admin
25+
from firebase_admin import _auth_utils
2526
from firebase_admin import _http_client
2627
from firebase_admin import _token_gen
2728
from firebase_admin import _user_import
@@ -76,7 +77,9 @@
7677
ListUsersPage = _user_mgt.ListUsersPage
7778
UserImportHash = _user_import.UserImportHash
7879
ImportUserRecord = _user_import.ImportUserRecord
80+
InvalidIdTokenError = _auth_utils.InvalidIdTokenError
7981
TokenSignError = _token_gen.TokenSignError
82+
UnexpectedResponseError = _auth_utils.UnexpectedResponseError
8083
UserImportResult = _user_import.UserImportResult
8184
UserInfo = _user_mgt.UserInfo
8285
UserMetadata = _user_mgt.UserMetadata
@@ -169,13 +172,10 @@ def create_session_cookie(id_token, expires_in, app=None):
169172
170173
Raises:
171174
ValueError: If input parameters are invalid.
172-
AuthError: If an error occurs while creating the cookie.
175+
FirebaseError: If an error occurs while creating the cookie.
173176
"""
174177
token_generator = _get_auth_service(app).token_generator
175-
try:
176-
return token_generator.create_session_cookie(id_token, expires_in)
177-
except _token_gen.ApiCallError as error:
178-
raise AuthError(error.code, str(error), error.detail)
178+
return token_generator.create_session_cookie(id_token, expires_in)
179179

180180

181181
def verify_session_cookie(session_cookie, check_revoked=False, app=None):

integration/test_auth.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,11 @@ def test_session_cookies(api_key):
129129
estimated_exp = int(time.time() + expires_in.total_seconds())
130130
assert abs(claims['exp'] - estimated_exp) < 5
131131

132+
def test_session_cookie_error():
133+
expires_in = datetime.timedelta(days=1)
134+
with pytest.raises(auth.InvalidIdTokenError):
135+
auth.create_session_cookie('not.a.token', expires_in=expires_in)
136+
132137
def test_get_non_existing_user():
133138
with pytest.raises(auth.AuthError) as excinfo:
134139
auth.get_user('non.existing')

tests/test_token_gen.py

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -301,17 +301,38 @@ def test_valid_args(self, user_mgt_app, expires_in):
301301
assert request == {'idToken' : 'id_token', 'validDuration': 3600}
302302

303303
def test_error(self, user_mgt_app):
304-
_instrument_user_manager(user_mgt_app, 500, '{"error":"test"}')
305-
with pytest.raises(auth.AuthError) as excinfo:
304+
_instrument_user_manager(user_mgt_app, 500, '{"error":{"message": "INVALID_ID_TOKEN"}}')
305+
with pytest.raises(auth.InvalidIdTokenError) as excinfo:
306+
auth.create_session_cookie('id_token', expires_in=3600, app=user_mgt_app)
307+
assert excinfo.value.code == exceptions.INVALID_ARGUMENT
308+
assert str(excinfo.value) == 'The provided ID token is invalid (INVALID_ID_TOKEN).'
309+
310+
def test_error_with_details(self, user_mgt_app):
311+
_instrument_user_manager(
312+
user_mgt_app, 500, '{"error":{"message": "INVALID_ID_TOKEN: More details."}}')
313+
with pytest.raises(auth.InvalidIdTokenError) as excinfo:
314+
auth.create_session_cookie('id_token', expires_in=3600, app=user_mgt_app)
315+
assert excinfo.value.code == exceptions.INVALID_ARGUMENT
316+
expected = 'The provided ID token is invalid (INVALID_ID_TOKEN). More details.'
317+
assert str(excinfo.value) == expected
318+
319+
def test_unexpected_error_code(self, user_mgt_app):
320+
_instrument_user_manager(user_mgt_app, 500, '{"error":{"message": "SOMETHING_UNUSUAL"}}')
321+
with pytest.raises(exceptions.InternalError) as excinfo:
322+
auth.create_session_cookie('id_token', expires_in=3600, app=user_mgt_app)
323+
assert str(excinfo.value) == 'Error while calling Auth service (SOMETHING_UNUSUAL).'
324+
325+
def test_unexpected_error_response(self, user_mgt_app):
326+
_instrument_user_manager(user_mgt_app, 500, '{}')
327+
with pytest.raises(exceptions.InternalError) as excinfo:
306328
auth.create_session_cookie('id_token', expires_in=3600, app=user_mgt_app)
307-
assert excinfo.value.code == _token_gen.COOKIE_CREATE_ERROR
308-
assert '{"error":"test"}' in str(excinfo.value)
329+
assert str(excinfo.value) == 'Unexpected error response: {}'
309330

310331
def test_unexpected_response(self, user_mgt_app):
311332
_instrument_user_manager(user_mgt_app, 200, '{}')
312-
with pytest.raises(auth.AuthError) as excinfo:
333+
with pytest.raises(auth.UnexpectedResponseError) as excinfo:
313334
auth.create_session_cookie('id_token', expires_in=3600, app=user_mgt_app)
314-
assert excinfo.value.code == _token_gen.COOKIE_CREATE_ERROR
335+
assert excinfo.value.code == exceptions.UNKNOWN
315336
assert 'Failed to create session cookie' in str(excinfo.value)
316337

317338

0 commit comments

Comments
 (0)