Skip to content

Commit 1210723

Browse files
authored
Migrated token verification APIs to new exception types (#317)
* Migrated token verification APIs to new error types * Removed old AuthError type * Added new exception types for revoked tokens
1 parent baf4991 commit 1210723

File tree

6 files changed

+207
-99
lines changed

6 files changed

+207
-99
lines changed

firebase_admin/_auth_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ class InvalidIdTokenError(exceptions.InvalidArgumentError):
216216

217217
default_message = 'The provided ID token is invalid'
218218

219-
def __init__(self, message, cause, http_response=None):
219+
def __init__(self, message, cause=None, http_response=None):
220220
exceptions.InvalidArgumentError.__init__(self, message, cause, http_response)
221221

222222

firebase_admin/_token_gen.py

Lines changed: 82 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -217,12 +217,18 @@ def __init__(self, app):
217217
project_id=app.project_id, short_name='ID token',
218218
operation='verify_id_token()',
219219
doc_url='https://firebase.google.com/docs/auth/admin/verify-id-tokens',
220-
cert_url=ID_TOKEN_CERT_URI, issuer=ID_TOKEN_ISSUER_PREFIX)
220+
cert_url=ID_TOKEN_CERT_URI,
221+
issuer=ID_TOKEN_ISSUER_PREFIX,
222+
invalid_token_error=_auth_utils.InvalidIdTokenError,
223+
expired_token_error=ExpiredIdTokenError)
221224
self.cookie_verifier = _JWTVerifier(
222225
project_id=app.project_id, short_name='session cookie',
223226
operation='verify_session_cookie()',
224227
doc_url='https://firebase.google.com/docs/auth/admin/verify-id-tokens',
225-
cert_url=COOKIE_CERT_URI, issuer=COOKIE_ISSUER_PREFIX)
228+
cert_url=COOKIE_CERT_URI,
229+
issuer=COOKIE_ISSUER_PREFIX,
230+
invalid_token_error=InvalidSessionCookieError,
231+
expired_token_error=ExpiredSessionCookieError)
226232

227233
def verify_id_token(self, id_token):
228234
return self.id_token_verifier.verify(id_token, self.request)
@@ -245,6 +251,8 @@ def __init__(self, **kwargs):
245251
self.articled_short_name = 'an {0}'.format(self.short_name)
246252
else:
247253
self.articled_short_name = 'a {0}'.format(self.short_name)
254+
self._invalid_token_error = kwargs.pop('invalid_token_error')
255+
self._expired_token_error = kwargs.pop('expired_token_error')
248256

249257
def verify(self, token, request):
250258
"""Verifies the signature and data for the provided JWT."""
@@ -261,8 +269,7 @@ def verify(self, token, request):
261269
'or set your Firebase project ID as an app option. Alternatively set the '
262270
'GOOGLE_CLOUD_PROJECT environment variable.'.format(self.operation))
263271

264-
header = jwt.decode_header(token)
265-
payload = jwt.decode(token, verify=False)
272+
header, payload = self._decode_unverified(token)
266273
issuer = payload.get('iss')
267274
audience = payload.get('aud')
268275
subject = payload.get('sub')
@@ -275,12 +282,12 @@ def verify(self, token, request):
275282
'See {0} for details on how to retrieve {1}.'.format(self.url, self.short_name))
276283

277284
error_message = None
278-
if not header.get('kid'):
279-
if audience == FIREBASE_AUDIENCE:
280-
error_message = (
281-
'{0} expects {1}, but was given a custom '
282-
'token.'.format(self.operation, self.articled_short_name))
283-
elif header.get('alg') == 'HS256' and payload.get(
285+
if audience == FIREBASE_AUDIENCE:
286+
error_message = (
287+
'{0} expects {1}, but was given a custom '
288+
'token.'.format(self.operation, self.articled_short_name))
289+
elif not header.get('kid'):
290+
if header.get('alg') == 'HS256' and payload.get(
284291
'v') is 0 and 'uid' in payload.get('d', {}):
285292
error_message = (
286293
'{0} expects {1}, but was given a legacy custom '
@@ -315,19 +322,76 @@ def verify(self, token, request):
315322
'{1}'.format(self.short_name, verify_id_token_msg))
316323

317324
if error_message:
318-
raise ValueError(error_message)
325+
raise self._invalid_token_error(error_message)
326+
327+
try:
328+
verified_claims = google.oauth2.id_token.verify_token(
329+
token,
330+
request=request,
331+
audience=self.project_id,
332+
certs_url=self.cert_url)
333+
verified_claims['uid'] = verified_claims['sub']
334+
return verified_claims
335+
except google.auth.exceptions.TransportError as error:
336+
raise CertificateFetchError(str(error), cause=error)
337+
except ValueError as error:
338+
if 'Token expired' in str(error):
339+
raise self._expired_token_error(str(error), cause=error)
340+
raise self._invalid_token_error(str(error), cause=error)
319341

320-
verified_claims = google.oauth2.id_token.verify_token(
321-
token,
322-
request=request,
323-
audience=self.project_id,
324-
certs_url=self.cert_url)
325-
verified_claims['uid'] = verified_claims['sub']
326-
return verified_claims
342+
def _decode_unverified(self, token):
343+
try:
344+
header = jwt.decode_header(token)
345+
payload = jwt.decode(token, verify=False)
346+
return header, payload
347+
except ValueError as error:
348+
raise self._invalid_token_error(str(error), cause=error)
327349

328350

329351
class TokenSignError(exceptions.UnknownError):
330352
"""Unexpected error while signing a Firebase custom token."""
331353

332354
def __init__(self, message, cause):
333355
exceptions.UnknownError.__init__(self, message, cause)
356+
357+
358+
class CertificateFetchError(exceptions.UnknownError):
359+
"""Failed to fetch some public key certificates required to verify a token."""
360+
361+
def __init__(self, message, cause):
362+
exceptions.UnknownError.__init__(self, message, cause)
363+
364+
365+
class ExpiredIdTokenError(_auth_utils.InvalidIdTokenError):
366+
"""The provided ID token is expired."""
367+
368+
def __init__(self, message, cause):
369+
_auth_utils.InvalidIdTokenError.__init__(self, message, cause)
370+
371+
372+
class RevokedIdTokenError(_auth_utils.InvalidIdTokenError):
373+
"""The provided ID token has been revoked."""
374+
375+
def __init__(self, message):
376+
_auth_utils.InvalidIdTokenError.__init__(self, message)
377+
378+
379+
class InvalidSessionCookieError(exceptions.InvalidArgumentError):
380+
"""The provided string is not a valid Firebase session cookie."""
381+
382+
def __init__(self, message, cause=None):
383+
exceptions.InvalidArgumentError.__init__(self, message, cause)
384+
385+
386+
class ExpiredSessionCookieError(InvalidSessionCookieError):
387+
"""The provided session cookie is expired."""
388+
389+
def __init__(self, message, cause):
390+
InvalidSessionCookieError.__init__(self, message, cause)
391+
392+
393+
class RevokedSessionCookieError(InvalidSessionCookieError):
394+
"""The provided session cookie has been revoked."""
395+
396+
def __init__(self, message):
397+
InvalidSessionCookieError.__init__(self, message)

firebase_admin/auth.py

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,23 @@
3131

3232

3333
_AUTH_ATTRIBUTE = '_auth'
34-
_ID_TOKEN_REVOKED = 'ID_TOKEN_REVOKED'
35-
_SESSION_COOKIE_REVOKED = 'SESSION_COOKIE_REVOKED'
3634

3735

3836
__all__ = [
3937
'ActionCodeSettings',
40-
'AuthError',
38+
'CertificateFetchError',
4139
'DELETE_ATTRIBUTE',
4240
'ErrorInfo',
41+
'ExpiredIdTokenError',
42+
'ExpiredSessionCookieError',
4343
'ExportedUserRecord',
4444
'ImportUserRecord',
4545
'InvalidDynamicLinkDomainError',
4646
'InvalidIdTokenError',
47+
'InvalidSessionCookieError',
4748
'ListUsersPage',
49+
'RevokedIdTokenError',
50+
'RevokedSessionCookieError',
4851
'TokenSignError',
4952
'UidAlreadyExistsError',
5053
'UnexpectedResponseError',
@@ -76,17 +79,23 @@
7679
]
7780

7881
ActionCodeSettings = _user_mgt.ActionCodeSettings
82+
CertificateFetchError = _token_gen.CertificateFetchError
7983
DELETE_ATTRIBUTE = _user_mgt.DELETE_ATTRIBUTE
8084
ErrorInfo = _user_import.ErrorInfo
85+
ExpiredIdTokenError = _token_gen.ExpiredIdTokenError
86+
ExpiredSessionCookieError = _token_gen.ExpiredSessionCookieError
8187
ExportedUserRecord = _user_mgt.ExportedUserRecord
82-
ListUsersPage = _user_mgt.ListUsersPage
83-
UserImportHash = _user_import.UserImportHash
8488
ImportUserRecord = _user_import.ImportUserRecord
8589
InvalidDynamicLinkDomainError = _auth_utils.InvalidDynamicLinkDomainError
8690
InvalidIdTokenError = _auth_utils.InvalidIdTokenError
91+
InvalidSessionCookieError = _token_gen.InvalidSessionCookieError
92+
ListUsersPage = _user_mgt.ListUsersPage
93+
RevokedIdTokenError = _token_gen.RevokedIdTokenError
94+
RevokedSessionCookieError = _token_gen.RevokedSessionCookieError
8795
TokenSignError = _token_gen.TokenSignError
8896
UidAlreadyExistsError = _auth_utils.UidAlreadyExistsError
8997
UnexpectedResponseError = _auth_utils.UnexpectedResponseError
98+
UserImportHash = _user_import.UserImportHash
9099
UserImportResult = _user_import.UserImportResult
91100
UserInfo = _user_mgt.UserInfo
92101
UserMetadata = _user_mgt.UserMetadata
@@ -149,9 +158,12 @@ def verify_id_token(id_token, app=None, check_revoked=False):
149158
dict: A dictionary of key-value pairs parsed from the decoded JWT.
150159
151160
Raises:
152-
ValueError: If the JWT was found to be invalid, or if the App's project ID cannot
153-
be determined.
154-
AuthError: If ``check_revoked`` is requested and the token was revoked.
161+
ValueError: If ``id_token`` is a not a string or is empty.
162+
InvalidIdTokenError: If ``id_token`` is not a valid Firebase ID token.
163+
ExpiredIdTokenError: If the specified ID token has expired.
164+
RevokedIdTokenError: If ``check_revoked`` is ``True`` and the ID token has been revoked.
165+
CertificateFetchError: If an error occurs while fetching the public key certificates
166+
required to verify the ID token.
155167
"""
156168
if not isinstance(check_revoked, bool):
157169
# guard against accidental wrong assignment.
@@ -160,7 +172,7 @@ def verify_id_token(id_token, app=None, check_revoked=False):
160172
token_verifier = _get_auth_service(app).token_verifier
161173
verified_claims = token_verifier.verify_id_token(id_token)
162174
if check_revoked:
163-
_check_jwt_revoked(verified_claims, _ID_TOKEN_REVOKED, 'ID token', app)
175+
_check_jwt_revoked(verified_claims, RevokedIdTokenError, 'ID token', app)
164176
return verified_claims
165177

166178

@@ -201,14 +213,17 @@ def verify_session_cookie(session_cookie, check_revoked=False, app=None):
201213
dict: A dictionary of key-value pairs parsed from the decoded JWT.
202214
203215
Raises:
204-
ValueError: If the cookie was found to be invalid, or if the App's project ID cannot
205-
be determined.
206-
AuthError: If ``check_revoked`` is requested and the cookie was revoked.
216+
ValueError: If ``session_cookie`` is a not a string or is empty.
217+
InvalidSessionCookieError: If ``session_cookie`` is not a valid Firebase session cookie.
218+
ExpiredSessionCookieError: If the specified session cookie has expired.
219+
RevokedSessionCookieError: If ``check_revoked`` is ``True`` and the cookie has been revoked.
220+
CertificateFetchError: If an error occurs while fetching the public key certificates
221+
required to verify the session cookie.
207222
"""
208223
token_verifier = _get_auth_service(app).token_verifier
209224
verified_claims = token_verifier.verify_session_cookie(session_cookie)
210225
if check_revoked:
211-
_check_jwt_revoked(verified_claims, _SESSION_COOKIE_REVOKED, 'session cookie', app)
226+
_check_jwt_revoked(verified_claims, RevokedSessionCookieError, 'session cookie', app)
212227
return verified_claims
213228

214229

@@ -513,19 +528,10 @@ def generate_sign_in_with_email_link(email, action_code_settings, app=None):
513528
'EMAIL_SIGNIN', email, action_code_settings=action_code_settings)
514529

515530

516-
def _check_jwt_revoked(verified_claims, error_code, label, app):
531+
def _check_jwt_revoked(verified_claims, exc_type, label, app):
517532
user = get_user(verified_claims.get('uid'), app=app)
518533
if verified_claims.get('iat') * 1000 < user.tokens_valid_after_timestamp:
519-
raise AuthError(error_code, 'The Firebase {0} has been revoked.'.format(label))
520-
521-
522-
class AuthError(Exception):
523-
"""Represents an Exception encountered while invoking the Firebase auth API."""
524-
525-
def __init__(self, code, message, error=None):
526-
Exception.__init__(self, message)
527-
self.code = code
528-
self.detail = error
534+
raise exc_type('The Firebase {0} has been revoked.'.format(label))
529535

530536

531537
class _AuthService(object):

integration/test_auth.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -351,9 +351,8 @@ def test_verify_id_token_revoked(new_user, api_key):
351351
# verify_id_token succeeded because it didn't check revoked.
352352
assert claims['iat'] * 1000 < user.tokens_valid_after_timestamp
353353

354-
with pytest.raises(auth.AuthError) as excinfo:
354+
with pytest.raises(auth.RevokedIdTokenError) as excinfo:
355355
claims = auth.verify_id_token(id_token, check_revoked=True)
356-
assert excinfo.value.code == auth._ID_TOKEN_REVOKED
357356
assert str(excinfo.value) == 'The Firebase ID token has been revoked.'
358357

359358
# Sign in again, verify works.
@@ -373,9 +372,8 @@ def test_verify_session_cookie_revoked(new_user, api_key):
373372
# verify_session_cookie succeeded because it didn't check revoked.
374373
assert claims['iat'] * 1000 < user.tokens_valid_after_timestamp
375374

376-
with pytest.raises(auth.AuthError) as excinfo:
375+
with pytest.raises(auth.RevokedSessionCookieError) as excinfo:
377376
claims = auth.verify_session_cookie(session_cookie, check_revoked=True)
378-
assert excinfo.value.code == auth._SESSION_COOKIE_REVOKED
379377
assert str(excinfo.value) == 'The Firebase session cookie has been revoked.'
380378

381379
# Sign in again, verify works.

0 commit comments

Comments
 (0)