diff --git a/firebase_admin/__init__.py b/firebase_admin/__init__.py index e2c8f1ec5..0ca82ec5e 100644 --- a/firebase_admin/__init__.py +++ b/firebase_admin/__init__.py @@ -50,8 +50,9 @@ def initialize_app(credential=None, options=None, name=_DEFAULT_APP_NAME): Google Application Default Credentials are used. options: A dictionary of configuration options (optional). Supported options include ``databaseURL``, ``storageBucket``, ``projectId``, ``databaseAuthVariableOverride``, - ``serviceAccountId`` and ``httpTimeout``. If ``httpTimeout`` is not set, the SDK - uses a default timeout of 120 seconds. + ``serviceAccountId`` and ``httpTimeout``. If ``httpTimeout`` is not set, the SDK uses + a default timeout of 120 seconds. + name: Name of the app (optional). Returns: App: A newly initialized instance of App. diff --git a/firebase_admin/_auth_client.py b/firebase_admin/_auth_client.py index 0fc9d2bee..38b42993a 100644 --- a/firebase_admin/_auth_client.py +++ b/firebase_admin/_auth_client.py @@ -92,7 +92,7 @@ def create_custom_token(self, uid, developer_claims=None): return self._token_generator.create_custom_token( uid, developer_claims, tenant_id=self.tenant_id) - def verify_id_token(self, id_token, check_revoked=False): + def verify_id_token(self, id_token, check_revoked=False, clock_skew_seconds=0): """Verifies the signature and data for the provided JWT. 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): id_token: A string of the encoded JWT. check_revoked: Boolean, If true, checks whether the token has been revoked or the user disabled (optional). + clock_skew_seconds: The number of seconds to tolerate when checking the token. + Must be between 0-60. Defaults to 0. Returns: 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): raise ValueError('Illegal check_revoked argument. Argument must be of type ' ' bool, but given "{0}".'.format(type(check_revoked))) - verified_claims = self._token_verifier.verify_id_token(id_token) + verified_claims = self._token_verifier.verify_id_token(id_token, clock_skew_seconds) if self.tenant_id: token_tenant_id = verified_claims.get('firebase', {}).get('tenant') if self.tenant_id != token_tenant_id: diff --git a/firebase_admin/_token_gen.py b/firebase_admin/_token_gen.py index 32c109d5d..a2fc725e8 100644 --- a/firebase_admin/_token_gen.py +++ b/firebase_admin/_token_gen.py @@ -289,11 +289,11 @@ def __init__(self, app): invalid_token_error=InvalidSessionCookieError, expired_token_error=ExpiredSessionCookieError) - def verify_id_token(self, id_token): - return self.id_token_verifier.verify(id_token, self.request) + def verify_id_token(self, id_token, clock_skew_seconds=0): + return self.id_token_verifier.verify(id_token, self.request, clock_skew_seconds) - def verify_session_cookie(self, cookie): - return self.cookie_verifier.verify(cookie, self.request) + def verify_session_cookie(self, cookie, clock_skew_seconds=0): + return self.cookie_verifier.verify(cookie, self.request, clock_skew_seconds) class _JWTVerifier: @@ -313,7 +313,7 @@ def __init__(self, **kwargs): self._invalid_token_error = kwargs.pop('invalid_token_error') self._expired_token_error = kwargs.pop('expired_token_error') - def verify(self, token, request): + def verify(self, token, request, clock_skew_seconds=0): """Verifies the signature and data for the provided JWT.""" token = token.encode('utf-8') if isinstance(token, str) else token if not isinstance(token, bytes) or not token: @@ -328,6 +328,11 @@ def verify(self, token, request): 'or set your Firebase project ID as an app option. Alternatively set the ' 'GOOGLE_CLOUD_PROJECT environment variable.'.format(self.operation)) + if clock_skew_seconds < 0 or clock_skew_seconds > 60: + raise ValueError( + 'Illegal clock_skew_seconds value: {0}. Must be between 0 and 60, inclusive.' + .format(clock_skew_seconds)) + header, payload = self._decode_unverified(token) issuer = payload.get('iss') audience = payload.get('aud') @@ -393,7 +398,8 @@ def verify(self, token, request): token, request=request, audience=self.project_id, - certs_url=self.cert_url) + certs_url=self.cert_url, + clock_skew_in_seconds=clock_skew_seconds) verified_claims['uid'] = verified_claims['sub'] return verified_claims except google.auth.exceptions.TransportError as error: diff --git a/firebase_admin/auth.py b/firebase_admin/auth.py index 6902a322f..84873c3da 100644 --- a/firebase_admin/auth.py +++ b/firebase_admin/auth.py @@ -191,7 +191,7 @@ def create_custom_token(uid, developer_claims=None, app=None): return client.create_custom_token(uid, developer_claims) -def verify_id_token(id_token, app=None, check_revoked=False): +def verify_id_token(id_token, app=None, check_revoked=False, clock_skew_seconds=0): """Verifies the signature and data for the provided JWT. 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): app: An App instance (optional). check_revoked: Boolean, If true, checks whether the token has been revoked or the user disabled (optional). - + clock_skew_seconds: The number of seconds to tolerate when checking the token. + Must be between 0-60. Defaults to 0. Returns: dict: A dictionary of key-value pairs parsed from the decoded JWT. @@ -217,7 +218,8 @@ def verify_id_token(id_token, app=None, check_revoked=False): record is disabled. """ client = _get_client(app) - return client.verify_id_token(id_token, check_revoked=check_revoked) + return client.verify_id_token( + id_token, check_revoked=check_revoked, clock_skew_seconds=clock_skew_seconds) def create_session_cookie(id_token, expires_in, app=None): @@ -243,7 +245,7 @@ def create_session_cookie(id_token, expires_in, app=None): return client._token_generator.create_session_cookie(id_token, expires_in) -def verify_session_cookie(session_cookie, check_revoked=False, app=None): +def verify_session_cookie(session_cookie, check_revoked=False, app=None, clock_skew_seconds=0): """Verifies a Firebase session cookie. 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): check_revoked: Boolean, if true, checks whether the cookie has been revoked or the user disabled (optional). app: An App instance (optional). + clock_skew_seconds: The number of seconds to tolerate when checking the cookie. Returns: 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): """ client = _get_client(app) # pylint: disable=protected-access - verified_claims = client._token_verifier.verify_session_cookie(session_cookie) + verified_claims = client._token_verifier.verify_session_cookie( + session_cookie, clock_skew_seconds) if check_revoked: client._check_jwt_revoked_or_disabled( verified_claims, RevokedSessionCookieError, 'session cookie') diff --git a/integration/test_auth.py b/integration/test_auth.py index 82974732d..e1d01a254 100644 --- a/integration/test_auth.py +++ b/integration/test_auth.py @@ -617,6 +617,7 @@ def test_verify_session_cookie_revoked(new_user, api_key): claims = auth.verify_session_cookie(session_cookie, check_revoked=True) assert claims['iat'] * 1000 >= user.tokens_valid_after_timestamp + def test_verify_session_cookie_disabled(new_user, api_key): custom_token = auth.create_custom_token(new_user.uid) id_token = _sign_in(custom_token, api_key) diff --git a/tests/test_token_gen.py b/tests/test_token_gen.py index 00b7956fa..64540f26f 100644 --- a/tests/test_token_gen.py +++ b/tests/test_token_gen.py @@ -440,6 +440,10 @@ class TestVerifyIdToken: 'iat': int(time.time()) - 10000, 'exp': int(time.time()) - 3600 }), + 'ExpiredTokenShort': _get_id_token({ + 'iat': int(time.time()) - 10000, + 'exp': int(time.time()) - 30 + }), 'BadFormatToken': 'foobar' } @@ -447,7 +451,8 @@ class TestVerifyIdToken: 'NoKid', 'WrongKid', 'FutureToken', - 'ExpiredToken' + 'ExpiredToken', + 'ExpiredTokenShort', ] def _assert_valid_token(self, id_token, app): @@ -555,6 +560,20 @@ def test_expired_token(self, user_mgt_app): assert excinfo.value.cause is not None assert excinfo.value.http_response is None + def test_expired_token_with_tolerance(self, user_mgt_app): + _overwrite_cert_request(user_mgt_app, MOCK_REQUEST) + id_token = self.invalid_tokens['ExpiredTokenShort'] + if _is_emulated(): + self._assert_valid_token(id_token, user_mgt_app) + return + claims = auth.verify_id_token(id_token, app=user_mgt_app, + clock_skew_seconds=60) + assert claims['admin'] is True + assert claims['uid'] == claims['sub'] + with pytest.raises(auth.ExpiredIdTokenError): + auth.verify_id_token(id_token, app=user_mgt_app, + clock_skew_seconds=20) + def test_project_id_option(self): app = firebase_admin.initialize_app( testutils.MockCredential(), options={'projectId': 'mock-project-id'}, name='myApp') @@ -619,6 +638,10 @@ class TestVerifySessionCookie: 'iat': int(time.time()) - 10000, 'exp': int(time.time()) - 3600 }), + 'ExpiredCookieShort': _get_session_cookie({ + 'iat': int(time.time()) - 10000, + 'exp': int(time.time()) - 30 + }), 'BadFormatCookie': 'foobar', 'IDToken': TEST_ID_TOKEN, } @@ -627,7 +650,8 @@ class TestVerifySessionCookie: 'NoKid', 'WrongKid', 'FutureCookie', - 'ExpiredCookie' + 'ExpiredCookie', + 'ExpiredCookieShort', ] def _assert_valid_cookie(self, cookie, app, check_revoked=False): @@ -715,6 +739,20 @@ def test_expired_cookie(self, user_mgt_app): assert excinfo.value.cause is not None assert excinfo.value.http_response is None + def test_expired_cookie_with_tolerance(self, user_mgt_app): + _overwrite_cert_request(user_mgt_app, MOCK_REQUEST) + cookie = self.invalid_cookies['ExpiredCookieShort'] + if _is_emulated(): + self._assert_valid_cookie(cookie, user_mgt_app) + return + claims = auth.verify_session_cookie(cookie, app=user_mgt_app, check_revoked=False, + clock_skew_seconds=59) + assert claims['admin'] is True + assert claims['uid'] == claims['sub'] + with pytest.raises(auth.ExpiredSessionCookieError): + auth.verify_session_cookie(cookie, app=user_mgt_app, check_revoked=False, + clock_skew_seconds=29) + def test_project_id_option(self): app = firebase_admin.initialize_app( testutils.MockCredential(), options={'projectId': 'mock-project-id'}, name='myApp')