Skip to content

Commit 0e35c9a

Browse files
fix(auth): check if user disabled on check_revoked (#565)
* fix(auth): check if user disabled on check_revoked When `verify_session_cookie` or `verify_id_token` is called with `check_revoked` set to `True` we should also check if the user is disabled. If disabled the `UserDisabledError` is raised.
1 parent fb64981 commit 0e35c9a

File tree

6 files changed

+133
-6
lines changed

6 files changed

+133
-6
lines changed

firebase_admin/_auth_client.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ def verify_id_token(self, id_token, check_revoked=False):
100100
101101
Args:
102102
id_token: A string of the encoded JWT.
103-
check_revoked: Boolean, If true, checks whether the token has been revoked (optional).
103+
check_revoked: Boolean, If true, checks whether the token has been revoked or
104+
the user disabled (optional).
104105
105106
Returns:
106107
dict: A dictionary of key-value pairs parsed from the decoded JWT.
@@ -115,6 +116,8 @@ def verify_id_token(self, id_token, check_revoked=False):
115116
this ``Client`` instance.
116117
CertificateFetchError: If an error occurs while fetching the public key certificates
117118
required to verify the ID token.
119+
UserDisabledError: If ``check_revoked`` is ``True`` and the corresponding user
120+
record is disabled.
118121
"""
119122
if not isinstance(check_revoked, bool):
120123
# guard against accidental wrong assignment.
@@ -129,7 +132,8 @@ def verify_id_token(self, id_token, check_revoked=False):
129132
'Invalid tenant ID: {0}'.format(token_tenant_id))
130133

131134
if check_revoked:
132-
self._check_jwt_revoked(verified_claims, _token_gen.RevokedIdTokenError, 'ID token')
135+
self._check_jwt_revoked_or_disabled(
136+
verified_claims, _token_gen.RevokedIdTokenError, 'ID token')
133137
return verified_claims
134138

135139
def revoke_refresh_tokens(self, uid):
@@ -720,7 +724,9 @@ def list_saml_provider_configs(
720724
"""
721725
return self._provider_manager.list_saml_provider_configs(page_token, max_results)
722726

723-
def _check_jwt_revoked(self, verified_claims, exc_type, label):
727+
def _check_jwt_revoked_or_disabled(self, verified_claims, exc_type, label):
724728
user = self.get_user(verified_claims.get('uid'))
729+
if user.disabled:
730+
raise _auth_utils.UserDisabledError('The user record is disabled.')
725731
if verified_claims.get('iat') * 1000 < user.tokens_valid_after_timestamp:
726732
raise exc_type('The Firebase {0} has been revoked.'.format(label))

firebase_admin/_auth_utils.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,15 @@ def __init__(self, message, cause=None, http_response=None):
385385
exceptions.NotFoundError.__init__(self, message, cause, http_response)
386386

387387

388+
class UserDisabledError(exceptions.InvalidArgumentError):
389+
"""An operation failed due to a user record being disabled."""
390+
391+
default_message = 'The user record is disabled'
392+
393+
def __init__(self, message, cause=None, http_response=None):
394+
exceptions.InvalidArgumentError.__init__(self, message, cause, http_response)
395+
396+
388397
_CODE_TO_EXC_TYPE = {
389398
'CONFIGURATION_NOT_FOUND': ConfigurationNotFoundError,
390399
'DUPLICATE_EMAIL': EmailAlreadyExistsError,

firebase_admin/auth.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
'TokenSignError',
6363
'UidAlreadyExistsError',
6464
'UnexpectedResponseError',
65+
'UserDisabledError',
6566
'UserImportHash',
6667
'UserImportResult',
6768
'UserInfo',
@@ -135,6 +136,7 @@
135136
TokenSignError = _token_gen.TokenSignError
136137
UidAlreadyExistsError = _auth_utils.UidAlreadyExistsError
137138
UnexpectedResponseError = _auth_utils.UnexpectedResponseError
139+
UserDisabledError = _auth_utils.UserDisabledError
138140
UserImportHash = _user_import.UserImportHash
139141
UserImportResult = _user_import.UserImportResult
140142
UserInfo = _user_mgt.UserInfo
@@ -198,7 +200,8 @@ def verify_id_token(id_token, app=None, check_revoked=False):
198200
Args:
199201
id_token: A string of the encoded JWT.
200202
app: An App instance (optional).
201-
check_revoked: Boolean, If true, checks whether the token has been revoked (optional).
203+
check_revoked: Boolean, If true, checks whether the token has been revoked or
204+
the user disabled (optional).
202205
203206
Returns:
204207
dict: A dictionary of key-value pairs parsed from the decoded JWT.
@@ -210,6 +213,8 @@ def verify_id_token(id_token, app=None, check_revoked=False):
210213
RevokedIdTokenError: If ``check_revoked`` is ``True`` and the ID token has been revoked.
211214
CertificateFetchError: If an error occurs while fetching the public key certificates
212215
required to verify the ID token.
216+
UserDisabledError: If ``check_revoked`` is ``True`` and the corresponding user
217+
record is disabled.
213218
"""
214219
client = _get_client(app)
215220
return client.verify_id_token(id_token, check_revoked=check_revoked)
@@ -246,7 +251,8 @@ def verify_session_cookie(session_cookie, check_revoked=False, app=None):
246251
247252
Args:
248253
session_cookie: A session cookie string to verify.
249-
check_revoked: Boolean, if true, checks whether the cookie has been revoked (optional).
254+
check_revoked: Boolean, if true, checks whether the cookie has been revoked or the
255+
user disabled (optional).
250256
app: An App instance (optional).
251257
252258
Returns:
@@ -259,12 +265,15 @@ def verify_session_cookie(session_cookie, check_revoked=False, app=None):
259265
RevokedSessionCookieError: If ``check_revoked`` is ``True`` and the cookie has been revoked.
260266
CertificateFetchError: If an error occurs while fetching the public key certificates
261267
required to verify the session cookie.
268+
UserDisabledError: If ``check_revoked`` is ``True`` and the corresponding user
269+
record is disabled.
262270
"""
263271
client = _get_client(app)
264272
# pylint: disable=protected-access
265273
verified_claims = client._token_verifier.verify_session_cookie(session_cookie)
266274
if check_revoked:
267-
client._check_jwt_revoked(verified_claims, RevokedSessionCookieError, 'session cookie')
275+
client._check_jwt_revoked_or_disabled(
276+
verified_claims, RevokedSessionCookieError, 'session cookie')
268277
return verified_claims
269278

270279

integration/test_auth.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,24 @@ def test_verify_id_token_revoked(new_user, api_key):
569569
claims = auth.verify_id_token(id_token, check_revoked=True)
570570
assert claims['iat'] * 1000 >= user.tokens_valid_after_timestamp
571571

572+
def test_verify_id_token_disabled(new_user, api_key):
573+
custom_token = auth.create_custom_token(new_user.uid)
574+
id_token = _sign_in(custom_token, api_key)
575+
claims = auth.verify_id_token(id_token, check_revoked=True)
576+
577+
# Disable the user record.
578+
auth.update_user(new_user.uid, disabled=True)
579+
# Verify the ID token without checking revocation. This should
580+
# not raise.
581+
claims = auth.verify_id_token(id_token, check_revoked=False)
582+
assert claims['sub'] == new_user.uid
583+
584+
# Verify the ID token while checking revocation. This should
585+
# raise an exception.
586+
with pytest.raises(auth.UserDisabledError) as excinfo:
587+
auth.verify_id_token(id_token, check_revoked=True)
588+
assert str(excinfo.value) == 'The user record is disabled.'
589+
572590
def test_verify_session_cookie_revoked(new_user, api_key):
573591
custom_token = auth.create_custom_token(new_user.uid)
574592
id_token = _sign_in(custom_token, api_key)
@@ -591,6 +609,24 @@ def test_verify_session_cookie_revoked(new_user, api_key):
591609
claims = auth.verify_session_cookie(session_cookie, check_revoked=True)
592610
assert claims['iat'] * 1000 >= user.tokens_valid_after_timestamp
593611

612+
def test_verify_session_cookie_disabled(new_user, api_key):
613+
custom_token = auth.create_custom_token(new_user.uid)
614+
id_token = _sign_in(custom_token, api_key)
615+
session_cookie = auth.create_session_cookie(id_token, expires_in=datetime.timedelta(days=1))
616+
617+
# Disable the user record.
618+
auth.update_user(new_user.uid, disabled=True)
619+
# Verify the session cookie without checking revocation. This should
620+
# not raise.
621+
claims = auth.verify_session_cookie(session_cookie, check_revoked=False)
622+
assert claims['sub'] == new_user.uid
623+
624+
# Verify the session cookie while checking revocation. This should
625+
# raise an exception.
626+
with pytest.raises(auth.UserDisabledError) as excinfo:
627+
auth.verify_session_cookie(session_cookie, check_revoked=True)
628+
assert str(excinfo.value) == 'The user record is disabled.'
629+
594630
def test_import_users():
595631
uid, email = _random_id()
596632
user = auth.ImportUserRecord(uid=uid, email=email)

snippets/auth/index.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,9 @@ def verify_token_uid_check_revoke(id_token):
150150
except auth.RevokedIdTokenError:
151151
# Token revoked, inform the user to reauthenticate or signOut().
152152
pass
153+
except auth.UserDisabledError:
154+
# Token belongs to a disabled user record.
155+
pass
153156
except auth.InvalidIdTokenError:
154157
# Token is invalid
155158
pass
@@ -1027,6 +1030,9 @@ def verify_id_token_and_check_revoked_tenant(tenant_client, id_token):
10271030
except auth.RevokedIdTokenError:
10281031
# Token revoked, inform the user to reauthenticate or signOut().
10291032
pass
1033+
except auth.UserDisabledError:
1034+
# Token belongs to a disabled user record.
1035+
pass
10301036
except auth.InvalidIdTokenError:
10311037
# Token is invalid
10321038
pass

tests/test_token_gen.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,19 @@ def revoked_tokens():
208208
mock_user['users'][0]['validSince'] = str(int(time.time())+100)
209209
return json.dumps(mock_user)
210210

211+
@pytest.fixture(scope='module')
212+
def user_disabled():
213+
mock_user = json.loads(testutils.resource('get_user.json'))
214+
mock_user['users'][0]['disabled'] = True
215+
return json.dumps(mock_user)
216+
217+
@pytest.fixture(scope='module')
218+
def user_disabled_and_revoked():
219+
mock_user = json.loads(testutils.resource('get_user.json'))
220+
mock_user['users'][0]['disabled'] = True
221+
mock_user['users'][0]['validSince'] = str(int(time.time())+100)
222+
return json.dumps(mock_user)
223+
211224

212225
class TestCreateCustomToken:
213226

@@ -471,6 +484,23 @@ def test_revoked_token_check_revoked(self, user_mgt_app, revoked_tokens, id_toke
471484
auth.verify_id_token(id_token, app=user_mgt_app, check_revoked=True)
472485
assert str(excinfo.value) == 'The Firebase ID token has been revoked.'
473486

487+
@pytest.mark.parametrize('id_token', valid_tokens.values(), ids=list(valid_tokens))
488+
def test_disabled_user_check_revoked(self, user_mgt_app, user_disabled, id_token):
489+
_overwrite_cert_request(user_mgt_app, MOCK_REQUEST)
490+
_instrument_user_manager(user_mgt_app, 200, user_disabled)
491+
with pytest.raises(auth.UserDisabledError) as excinfo:
492+
auth.verify_id_token(id_token, app=user_mgt_app, check_revoked=True)
493+
assert str(excinfo.value) == 'The user record is disabled.'
494+
495+
@pytest.mark.parametrize('id_token', valid_tokens.values(), ids=list(valid_tokens))
496+
def test_check_disabled_before_revoked(
497+
self, user_mgt_app, user_disabled_and_revoked, id_token):
498+
_overwrite_cert_request(user_mgt_app, MOCK_REQUEST)
499+
_instrument_user_manager(user_mgt_app, 200, user_disabled_and_revoked)
500+
with pytest.raises(auth.UserDisabledError) as excinfo:
501+
auth.verify_id_token(id_token, app=user_mgt_app, check_revoked=True)
502+
assert str(excinfo.value) == 'The user record is disabled.'
503+
474504
@pytest.mark.parametrize('arg', INVALID_BOOLS)
475505
def test_invalid_check_revoked(self, user_mgt_app, arg):
476506
_overwrite_cert_request(user_mgt_app, MOCK_REQUEST)
@@ -485,6 +515,14 @@ def test_revoked_token_do_not_check_revoked(self, user_mgt_app, revoked_tokens,
485515
assert claims['admin'] is True
486516
assert claims['uid'] == claims['sub']
487517

518+
@pytest.mark.parametrize('id_token', valid_tokens.values(), ids=list(valid_tokens))
519+
def test_disabled_user_do_not_check_revoked(self, user_mgt_app, user_disabled, id_token):
520+
_overwrite_cert_request(user_mgt_app, MOCK_REQUEST)
521+
_instrument_user_manager(user_mgt_app, 200, user_disabled)
522+
claims = auth.verify_id_token(id_token, app=user_mgt_app, check_revoked=False)
523+
assert claims['admin'] is True
524+
assert claims['uid'] == claims['sub']
525+
488526
@pytest.mark.parametrize('id_token', INVALID_JWT_ARGS.values(), ids=list(INVALID_JWT_ARGS))
489527
def test_invalid_arg(self, user_mgt_app, id_token):
490528
_overwrite_cert_request(user_mgt_app, MOCK_REQUEST)
@@ -622,6 +660,29 @@ def test_revoked_cookie_does_not_check_revoked(self, user_mgt_app, revoked_token
622660
_instrument_user_manager(user_mgt_app, 200, revoked_tokens)
623661
self._assert_valid_cookie(cookie, app=user_mgt_app, check_revoked=False)
624662

663+
@pytest.mark.parametrize('cookie', valid_cookies.values(), ids=list(valid_cookies))
664+
def test_disabled_user_check_revoked(self, user_mgt_app, user_disabled, cookie):
665+
_overwrite_cert_request(user_mgt_app, MOCK_REQUEST)
666+
_instrument_user_manager(user_mgt_app, 200, user_disabled)
667+
with pytest.raises(auth.UserDisabledError) as excinfo:
668+
auth.verify_session_cookie(cookie, app=user_mgt_app, check_revoked=True)
669+
assert str(excinfo.value) == 'The user record is disabled.'
670+
671+
@pytest.mark.parametrize('cookie', valid_cookies.values(), ids=list(valid_cookies))
672+
def test_check_disabled_before_revoked(
673+
self, user_mgt_app, user_disabled_and_revoked, cookie):
674+
_overwrite_cert_request(user_mgt_app, MOCK_REQUEST)
675+
_instrument_user_manager(user_mgt_app, 200, user_disabled_and_revoked)
676+
with pytest.raises(auth.UserDisabledError) as excinfo:
677+
auth.verify_session_cookie(cookie, app=user_mgt_app, check_revoked=True)
678+
assert str(excinfo.value) == 'The user record is disabled.'
679+
680+
@pytest.mark.parametrize('cookie', valid_cookies.values(), ids=list(valid_cookies))
681+
def test_disabled_user_does_not_check_revoked(self, user_mgt_app, user_disabled, cookie):
682+
_overwrite_cert_request(user_mgt_app, MOCK_REQUEST)
683+
_instrument_user_manager(user_mgt_app, 200, user_disabled)
684+
self._assert_valid_cookie(cookie, app=user_mgt_app, check_revoked=False)
685+
625686
@pytest.mark.parametrize('cookie', INVALID_JWT_ARGS.values(), ids=list(INVALID_JWT_ARGS))
626687
def test_invalid_args(self, user_mgt_app, cookie):
627688
_overwrite_cert_request(user_mgt_app, MOCK_REQUEST)

0 commit comments

Comments
 (0)