Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(auth): Add clockSkewInSeconds #714

Merged
merged 9 commits into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions firebase_admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 4 additions & 2 deletions firebase_admin/_auth_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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:
Expand Down
18 changes: 12 additions & 6 deletions firebase_admin/_token_gen.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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')
Expand Down Expand Up @@ -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:
Expand Down
14 changes: 9 additions & 5 deletions firebase_admin/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.

Expand All @@ -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):
Expand All @@ -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
Expand All @@ -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.
Expand All @@ -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')
Expand Down
1 change: 1 addition & 0 deletions integration/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
42 changes: 40 additions & 2 deletions tests/test_token_gen.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,14 +440,19 @@ 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'
}

tokens_accepted_in_emulator = [
'NoKid',
'WrongKid',
'FutureToken',
'ExpiredToken'
'ExpiredToken',
'ExpiredTokenShort',
]

def _assert_valid_token(self, id_token, app):
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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,
}
Expand All @@ -627,7 +650,8 @@ class TestVerifySessionCookie:
'NoKid',
'WrongKid',
'FutureCookie',
'ExpiredCookie'
'ExpiredCookie',
'ExpiredCookieShort',
]

def _assert_valid_cookie(self, cookie, app, check_revoked=False):
Expand Down Expand Up @@ -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')
Expand Down