Skip to content

Commit 44b7568

Browse files
authored
feat: add clockSkewSeconds (#714)
* feat: add clockSkewSeconds per feedback in #625 (comment) adds unit and integration tests as well. unit tests and lint pass. * fix: test * chore: version bump for testing * chore: address CR * fix:lint * chore: address CR * chore: remove test * fix: remove more tests * chore: address CR
1 parent 4052a3c commit 44b7568

File tree

6 files changed

+69
-17
lines changed

6 files changed

+69
-17
lines changed

firebase_admin/__init__.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,9 @@ def initialize_app(credential=None, options=None, name=_DEFAULT_APP_NAME):
5050
Google Application Default Credentials are used.
5151
options: A dictionary of configuration options (optional). Supported options include
5252
``databaseURL``, ``storageBucket``, ``projectId``, ``databaseAuthVariableOverride``,
53-
``serviceAccountId`` and ``httpTimeout``. If ``httpTimeout`` is not set, the SDK
54-
uses a default timeout of 120 seconds.
53+
``serviceAccountId`` and ``httpTimeout``. If ``httpTimeout`` is not set, the SDK uses
54+
a default timeout of 120 seconds.
55+
5556
name: Name of the app (optional).
5657
Returns:
5758
App: A newly initialized instance of App.

firebase_admin/_auth_client.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ def create_custom_token(self, uid, developer_claims=None):
9292
return self._token_generator.create_custom_token(
9393
uid, developer_claims, tenant_id=self.tenant_id)
9494

95-
def verify_id_token(self, id_token, check_revoked=False):
95+
def verify_id_token(self, id_token, check_revoked=False, clock_skew_seconds=0):
9696
"""Verifies the signature and data for the provided JWT.
9797
9898
Accepts a signed token string, verifies that it is current, was issued
@@ -102,6 +102,8 @@ def verify_id_token(self, id_token, check_revoked=False):
102102
id_token: A string of the encoded JWT.
103103
check_revoked: Boolean, If true, checks whether the token has been revoked or
104104
the user disabled (optional).
105+
clock_skew_seconds: The number of seconds to tolerate when checking the token.
106+
Must be between 0-60. Defaults to 0.
105107
106108
Returns:
107109
dict: A dictionary of key-value pairs parsed from the decoded JWT.
@@ -124,7 +126,7 @@ def verify_id_token(self, id_token, check_revoked=False):
124126
raise ValueError('Illegal check_revoked argument. Argument must be of type '
125127
' bool, but given "{0}".'.format(type(check_revoked)))
126128

127-
verified_claims = self._token_verifier.verify_id_token(id_token)
129+
verified_claims = self._token_verifier.verify_id_token(id_token, clock_skew_seconds)
128130
if self.tenant_id:
129131
token_tenant_id = verified_claims.get('firebase', {}).get('tenant')
130132
if self.tenant_id != token_tenant_id:

firebase_admin/_token_gen.py

+12-6
Original file line numberDiff line numberDiff line change
@@ -289,11 +289,11 @@ def __init__(self, app):
289289
invalid_token_error=InvalidSessionCookieError,
290290
expired_token_error=ExpiredSessionCookieError)
291291

292-
def verify_id_token(self, id_token):
293-
return self.id_token_verifier.verify(id_token, self.request)
292+
def verify_id_token(self, id_token, clock_skew_seconds=0):
293+
return self.id_token_verifier.verify(id_token, self.request, clock_skew_seconds)
294294

295-
def verify_session_cookie(self, cookie):
296-
return self.cookie_verifier.verify(cookie, self.request)
295+
def verify_session_cookie(self, cookie, clock_skew_seconds=0):
296+
return self.cookie_verifier.verify(cookie, self.request, clock_skew_seconds)
297297

298298

299299
class _JWTVerifier:
@@ -313,7 +313,7 @@ def __init__(self, **kwargs):
313313
self._invalid_token_error = kwargs.pop('invalid_token_error')
314314
self._expired_token_error = kwargs.pop('expired_token_error')
315315

316-
def verify(self, token, request):
316+
def verify(self, token, request, clock_skew_seconds=0):
317317
"""Verifies the signature and data for the provided JWT."""
318318
token = token.encode('utf-8') if isinstance(token, str) else token
319319
if not isinstance(token, bytes) or not token:
@@ -328,6 +328,11 @@ def verify(self, token, request):
328328
'or set your Firebase project ID as an app option. Alternatively set the '
329329
'GOOGLE_CLOUD_PROJECT environment variable.'.format(self.operation))
330330

331+
if clock_skew_seconds < 0 or clock_skew_seconds > 60:
332+
raise ValueError(
333+
'Illegal clock_skew_seconds value: {0}. Must be between 0 and 60, inclusive.'
334+
.format(clock_skew_seconds))
335+
331336
header, payload = self._decode_unverified(token)
332337
issuer = payload.get('iss')
333338
audience = payload.get('aud')
@@ -393,7 +398,8 @@ def verify(self, token, request):
393398
token,
394399
request=request,
395400
audience=self.project_id,
396-
certs_url=self.cert_url)
401+
certs_url=self.cert_url,
402+
clock_skew_in_seconds=clock_skew_seconds)
397403
verified_claims['uid'] = verified_claims['sub']
398404
return verified_claims
399405
except google.auth.exceptions.TransportError as error:

firebase_admin/auth.py

+9-5
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ def create_custom_token(uid, developer_claims=None, app=None):
191191
return client.create_custom_token(uid, developer_claims)
192192

193193

194-
def verify_id_token(id_token, app=None, check_revoked=False):
194+
def verify_id_token(id_token, app=None, check_revoked=False, clock_skew_seconds=0):
195195
"""Verifies the signature and data for the provided JWT.
196196
197197
Accepts a signed token string, verifies that it is current, and issued
@@ -202,7 +202,8 @@ def verify_id_token(id_token, app=None, check_revoked=False):
202202
app: An App instance (optional).
203203
check_revoked: Boolean, If true, checks whether the token has been revoked or
204204
the user disabled (optional).
205-
205+
clock_skew_seconds: The number of seconds to tolerate when checking the token.
206+
Must be between 0-60. Defaults to 0.
206207
Returns:
207208
dict: A dictionary of key-value pairs parsed from the decoded JWT.
208209
@@ -217,7 +218,8 @@ def verify_id_token(id_token, app=None, check_revoked=False):
217218
record is disabled.
218219
"""
219220
client = _get_client(app)
220-
return client.verify_id_token(id_token, check_revoked=check_revoked)
221+
return client.verify_id_token(
222+
id_token, check_revoked=check_revoked, clock_skew_seconds=clock_skew_seconds)
221223

222224

223225
def create_session_cookie(id_token, expires_in, app=None):
@@ -243,7 +245,7 @@ def create_session_cookie(id_token, expires_in, app=None):
243245
return client._token_generator.create_session_cookie(id_token, expires_in)
244246

245247

246-
def verify_session_cookie(session_cookie, check_revoked=False, app=None):
248+
def verify_session_cookie(session_cookie, check_revoked=False, app=None, clock_skew_seconds=0):
247249
"""Verifies a Firebase session cookie.
248250
249251
Accepts a session cookie string, verifies that it is current, and issued
@@ -254,6 +256,7 @@ def verify_session_cookie(session_cookie, check_revoked=False, app=None):
254256
check_revoked: Boolean, if true, checks whether the cookie has been revoked or the
255257
user disabled (optional).
256258
app: An App instance (optional).
259+
clock_skew_seconds: The number of seconds to tolerate when checking the cookie.
257260
258261
Returns:
259262
dict: A dictionary of key-value pairs parsed from the decoded JWT.
@@ -270,7 +273,8 @@ def verify_session_cookie(session_cookie, check_revoked=False, app=None):
270273
"""
271274
client = _get_client(app)
272275
# pylint: disable=protected-access
273-
verified_claims = client._token_verifier.verify_session_cookie(session_cookie)
276+
verified_claims = client._token_verifier.verify_session_cookie(
277+
session_cookie, clock_skew_seconds)
274278
if check_revoked:
275279
client._check_jwt_revoked_or_disabled(
276280
verified_claims, RevokedSessionCookieError, 'session cookie')

integration/test_auth.py

+1
Original file line numberDiff line numberDiff line change
@@ -617,6 +617,7 @@ def test_verify_session_cookie_revoked(new_user, api_key):
617617
claims = auth.verify_session_cookie(session_cookie, check_revoked=True)
618618
assert claims['iat'] * 1000 >= user.tokens_valid_after_timestamp
619619

620+
620621
def test_verify_session_cookie_disabled(new_user, api_key):
621622
custom_token = auth.create_custom_token(new_user.uid)
622623
id_token = _sign_in(custom_token, api_key)

tests/test_token_gen.py

+40-2
Original file line numberDiff line numberDiff line change
@@ -440,14 +440,19 @@ class TestVerifyIdToken:
440440
'iat': int(time.time()) - 10000,
441441
'exp': int(time.time()) - 3600
442442
}),
443+
'ExpiredTokenShort': _get_id_token({
444+
'iat': int(time.time()) - 10000,
445+
'exp': int(time.time()) - 30
446+
}),
443447
'BadFormatToken': 'foobar'
444448
}
445449

446450
tokens_accepted_in_emulator = [
447451
'NoKid',
448452
'WrongKid',
449453
'FutureToken',
450-
'ExpiredToken'
454+
'ExpiredToken',
455+
'ExpiredTokenShort',
451456
]
452457

453458
def _assert_valid_token(self, id_token, app):
@@ -555,6 +560,20 @@ def test_expired_token(self, user_mgt_app):
555560
assert excinfo.value.cause is not None
556561
assert excinfo.value.http_response is None
557562

563+
def test_expired_token_with_tolerance(self, user_mgt_app):
564+
_overwrite_cert_request(user_mgt_app, MOCK_REQUEST)
565+
id_token = self.invalid_tokens['ExpiredTokenShort']
566+
if _is_emulated():
567+
self._assert_valid_token(id_token, user_mgt_app)
568+
return
569+
claims = auth.verify_id_token(id_token, app=user_mgt_app,
570+
clock_skew_seconds=60)
571+
assert claims['admin'] is True
572+
assert claims['uid'] == claims['sub']
573+
with pytest.raises(auth.ExpiredIdTokenError):
574+
auth.verify_id_token(id_token, app=user_mgt_app,
575+
clock_skew_seconds=20)
576+
558577
def test_project_id_option(self):
559578
app = firebase_admin.initialize_app(
560579
testutils.MockCredential(), options={'projectId': 'mock-project-id'}, name='myApp')
@@ -619,6 +638,10 @@ class TestVerifySessionCookie:
619638
'iat': int(time.time()) - 10000,
620639
'exp': int(time.time()) - 3600
621640
}),
641+
'ExpiredCookieShort': _get_session_cookie({
642+
'iat': int(time.time()) - 10000,
643+
'exp': int(time.time()) - 30
644+
}),
622645
'BadFormatCookie': 'foobar',
623646
'IDToken': TEST_ID_TOKEN,
624647
}
@@ -627,7 +650,8 @@ class TestVerifySessionCookie:
627650
'NoKid',
628651
'WrongKid',
629652
'FutureCookie',
630-
'ExpiredCookie'
653+
'ExpiredCookie',
654+
'ExpiredCookieShort',
631655
]
632656

633657
def _assert_valid_cookie(self, cookie, app, check_revoked=False):
@@ -715,6 +739,20 @@ def test_expired_cookie(self, user_mgt_app):
715739
assert excinfo.value.cause is not None
716740
assert excinfo.value.http_response is None
717741

742+
def test_expired_cookie_with_tolerance(self, user_mgt_app):
743+
_overwrite_cert_request(user_mgt_app, MOCK_REQUEST)
744+
cookie = self.invalid_cookies['ExpiredCookieShort']
745+
if _is_emulated():
746+
self._assert_valid_cookie(cookie, user_mgt_app)
747+
return
748+
claims = auth.verify_session_cookie(cookie, app=user_mgt_app, check_revoked=False,
749+
clock_skew_seconds=59)
750+
assert claims['admin'] is True
751+
assert claims['uid'] == claims['sub']
752+
with pytest.raises(auth.ExpiredSessionCookieError):
753+
auth.verify_session_cookie(cookie, app=user_mgt_app, check_revoked=False,
754+
clock_skew_seconds=29)
755+
718756
def test_project_id_option(self):
719757
app = firebase_admin.initialize_app(
720758
testutils.MockCredential(), options={'projectId': 'mock-project-id'}, name='myApp')

0 commit comments

Comments
 (0)