Skip to content

Commit 8e95c1e

Browse files
fix: add clock_skew_in_seconds to verify_token functions (#894)
1 parent 2fa8cc5 commit 8e95c1e

File tree

4 files changed

+191
-12
lines changed

4 files changed

+191
-12
lines changed

google/oauth2/_id_token_async.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,11 @@ async def _fetch_certs(request, certs_url):
9999

100100

101101
async def verify_token(
102-
id_token, request, audience=None, certs_url=sync_id_token._GOOGLE_OAUTH2_CERTS_URL
102+
id_token,
103+
request,
104+
audience=None,
105+
certs_url=sync_id_token._GOOGLE_OAUTH2_CERTS_URL,
106+
clock_skew_in_seconds=0,
103107
):
104108
"""Verifies an ID token and returns the decoded token.
105109
@@ -112,16 +116,25 @@ async def verify_token(
112116
certs_url (str): The URL that specifies the certificates to use to
113117
verify the token. This URL should return JSON in the format of
114118
``{'key id': 'x509 certificate'}``.
119+
clock_skew_in_seconds (int): The clock skew used for `iat` and `exp`
120+
validation.
115121
116122
Returns:
117123
Mapping[str, Any]: The decoded token.
118124
"""
119125
certs = await _fetch_certs(request, certs_url)
120126

121-
return jwt.decode(id_token, certs=certs, audience=audience)
127+
return jwt.decode(
128+
id_token,
129+
certs=certs,
130+
audience=audience,
131+
clock_skew_in_seconds=clock_skew_in_seconds,
132+
)
122133

123134

124-
async def verify_oauth2_token(id_token, request, audience=None):
135+
async def verify_oauth2_token(
136+
id_token, request, audience=None, clock_skew_in_seconds=0
137+
):
125138
"""Verifies an ID Token issued by Google's OAuth 2.0 authorization server.
126139
127140
Args:
@@ -131,6 +144,8 @@ async def verify_oauth2_token(id_token, request, audience=None):
131144
audience (str): The audience that this token is intended for. This is
132145
typically your application's OAuth 2.0 client ID. If None then the
133146
audience is not verified.
147+
clock_skew_in_seconds (int): The clock skew used for `iat` and `exp`
148+
validation.
134149
135150
Returns:
136151
Mapping[str, Any]: The decoded token.
@@ -143,6 +158,7 @@ async def verify_oauth2_token(id_token, request, audience=None):
143158
request,
144159
audience=audience,
145160
certs_url=sync_id_token._GOOGLE_OAUTH2_CERTS_URL,
161+
clock_skew_in_seconds=clock_skew_in_seconds,
146162
)
147163

148164
if idinfo["iss"] not in sync_id_token._GOOGLE_ISSUERS:
@@ -155,7 +171,9 @@ async def verify_oauth2_token(id_token, request, audience=None):
155171
return idinfo
156172

157173

158-
async def verify_firebase_token(id_token, request, audience=None):
174+
async def verify_firebase_token(
175+
id_token, request, audience=None, clock_skew_in_seconds=0
176+
):
159177
"""Verifies an ID Token issued by Firebase Authentication.
160178
161179
Args:
@@ -165,6 +183,8 @@ async def verify_firebase_token(id_token, request, audience=None):
165183
audience (str): The audience that this token is intended for. This is
166184
typically your Firebase application ID. If None then the audience
167185
is not verified.
186+
clock_skew_in_seconds (int): The clock skew used for `iat` and `exp`
187+
validation.
168188
169189
Returns:
170190
Mapping[str, Any]: The decoded token.
@@ -174,6 +194,7 @@ async def verify_firebase_token(id_token, request, audience=None):
174194
request,
175195
audience=audience,
176196
certs_url=sync_id_token._GOOGLE_APIS_CERTS_URL,
197+
clock_skew_in_seconds=clock_skew_in_seconds,
177198
)
178199

179200

google/oauth2/id_token.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,13 @@ def _fetch_certs(request, certs_url):
105105
return json.loads(response.data.decode("utf-8"))
106106

107107

108-
def verify_token(id_token, request, audience=None, certs_url=_GOOGLE_OAUTH2_CERTS_URL):
108+
def verify_token(
109+
id_token,
110+
request,
111+
audience=None,
112+
certs_url=_GOOGLE_OAUTH2_CERTS_URL,
113+
clock_skew_in_seconds=0,
114+
):
109115
"""Verifies an ID token and returns the decoded token.
110116
111117
Args:
@@ -117,16 +123,23 @@ def verify_token(id_token, request, audience=None, certs_url=_GOOGLE_OAUTH2_CERT
117123
certs_url (str): The URL that specifies the certificates to use to
118124
verify the token. This URL should return JSON in the format of
119125
``{'key id': 'x509 certificate'}``.
126+
clock_skew_in_seconds (int): The clock skew used for `iat` and `exp`
127+
validation.
120128
121129
Returns:
122130
Mapping[str, Any]: The decoded token.
123131
"""
124132
certs = _fetch_certs(request, certs_url)
125133

126-
return jwt.decode(id_token, certs=certs, audience=audience)
134+
return jwt.decode(
135+
id_token,
136+
certs=certs,
137+
audience=audience,
138+
clock_skew_in_seconds=clock_skew_in_seconds,
139+
)
127140

128141

129-
def verify_oauth2_token(id_token, request, audience=None):
142+
def verify_oauth2_token(id_token, request, audience=None, clock_skew_in_seconds=0):
130143
"""Verifies an ID Token issued by Google's OAuth 2.0 authorization server.
131144
132145
Args:
@@ -136,6 +149,8 @@ def verify_oauth2_token(id_token, request, audience=None):
136149
audience (str): The audience that this token is intended for. This is
137150
typically your application's OAuth 2.0 client ID. If None then the
138151
audience is not verified.
152+
clock_skew_in_seconds (int): The clock skew used for `iat` and `exp`
153+
validation.
139154
140155
Returns:
141156
Mapping[str, Any]: The decoded token.
@@ -144,7 +159,11 @@ def verify_oauth2_token(id_token, request, audience=None):
144159
exceptions.GoogleAuthError: If the issuer is invalid.
145160
"""
146161
idinfo = verify_token(
147-
id_token, request, audience=audience, certs_url=_GOOGLE_OAUTH2_CERTS_URL
162+
id_token,
163+
request,
164+
audience=audience,
165+
certs_url=_GOOGLE_OAUTH2_CERTS_URL,
166+
clock_skew_in_seconds=clock_skew_in_seconds,
148167
)
149168

150169
if idinfo["iss"] not in _GOOGLE_ISSUERS:
@@ -157,7 +176,7 @@ def verify_oauth2_token(id_token, request, audience=None):
157176
return idinfo
158177

159178

160-
def verify_firebase_token(id_token, request, audience=None):
179+
def verify_firebase_token(id_token, request, audience=None, clock_skew_in_seconds=0):
161180
"""Verifies an ID Token issued by Firebase Authentication.
162181
163182
Args:
@@ -167,12 +186,18 @@ def verify_firebase_token(id_token, request, audience=None):
167186
audience (str): The audience that this token is intended for. This is
168187
typically your Firebase application ID. If None then the audience
169188
is not verified.
189+
clock_skew_in_seconds (int): The clock skew used for `iat` and `exp`
190+
validation.
170191
171192
Returns:
172193
Mapping[str, Any]: The decoded token.
173194
"""
174195
return verify_token(
175-
id_token, request, audience=audience, certs_url=_GOOGLE_APIS_CERTS_URL
196+
id_token,
197+
request,
198+
audience=audience,
199+
certs_url=_GOOGLE_APIS_CERTS_URL,
200+
clock_skew_in_seconds=clock_skew_in_seconds,
176201
)
177202

178203

tests/oauth2/test_id_token.py

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,10 @@ def test_verify_token(_fetch_certs, decode):
7171
mock.sentinel.request, id_token._GOOGLE_OAUTH2_CERTS_URL
7272
)
7373
decode.assert_called_once_with(
74-
mock.sentinel.token, certs=_fetch_certs.return_value, audience=None
74+
mock.sentinel.token,
75+
certs=_fetch_certs.return_value,
76+
audience=None,
77+
clock_skew_in_seconds=0,
7578
)
7679

7780

@@ -91,6 +94,28 @@ def test_verify_token_args(_fetch_certs, decode):
9194
mock.sentinel.token,
9295
certs=_fetch_certs.return_value,
9396
audience=mock.sentinel.audience,
97+
clock_skew_in_seconds=0,
98+
)
99+
100+
101+
@mock.patch("google.auth.jwt.decode", autospec=True)
102+
@mock.patch("google.oauth2.id_token._fetch_certs", autospec=True)
103+
def test_verify_token_clock_skew(_fetch_certs, decode):
104+
result = id_token.verify_token(
105+
mock.sentinel.token,
106+
mock.sentinel.request,
107+
audience=mock.sentinel.audience,
108+
certs_url=mock.sentinel.certs_url,
109+
clock_skew_in_seconds=10,
110+
)
111+
112+
assert result == decode.return_value
113+
_fetch_certs.assert_called_once_with(mock.sentinel.request, mock.sentinel.certs_url)
114+
decode.assert_called_once_with(
115+
mock.sentinel.token,
116+
certs=_fetch_certs.return_value,
117+
audience=mock.sentinel.audience,
118+
clock_skew_in_seconds=10,
94119
)
95120

96121

@@ -107,6 +132,27 @@ def test_verify_oauth2_token(verify_token):
107132
mock.sentinel.request,
108133
audience=mock.sentinel.audience,
109134
certs_url=id_token._GOOGLE_OAUTH2_CERTS_URL,
135+
clock_skew_in_seconds=0,
136+
)
137+
138+
139+
@mock.patch("google.oauth2.id_token.verify_token", autospec=True)
140+
def test_verify_oauth2_token_clock_skew(verify_token):
141+
verify_token.return_value = {"iss": "accounts.google.com"}
142+
result = id_token.verify_oauth2_token(
143+
mock.sentinel.token,
144+
mock.sentinel.request,
145+
audience=mock.sentinel.audience,
146+
clock_skew_in_seconds=10,
147+
)
148+
149+
assert result == verify_token.return_value
150+
verify_token.assert_called_once_with(
151+
mock.sentinel.token,
152+
mock.sentinel.request,
153+
audience=mock.sentinel.audience,
154+
certs_url=id_token._GOOGLE_OAUTH2_CERTS_URL,
155+
clock_skew_in_seconds=10,
110156
)
111157

112158

@@ -132,6 +178,26 @@ def test_verify_firebase_token(verify_token):
132178
mock.sentinel.request,
133179
audience=mock.sentinel.audience,
134180
certs_url=id_token._GOOGLE_APIS_CERTS_URL,
181+
clock_skew_in_seconds=0,
182+
)
183+
184+
185+
@mock.patch("google.oauth2.id_token.verify_token", autospec=True)
186+
def test_verify_firebase_token_clock_skew(verify_token):
187+
result = id_token.verify_firebase_token(
188+
mock.sentinel.token,
189+
mock.sentinel.request,
190+
audience=mock.sentinel.audience,
191+
clock_skew_in_seconds=10,
192+
)
193+
194+
assert result == verify_token.return_value
195+
verify_token.assert_called_once_with(
196+
mock.sentinel.token,
197+
mock.sentinel.request,
198+
audience=mock.sentinel.audience,
199+
certs_url=id_token._GOOGLE_APIS_CERTS_URL,
200+
clock_skew_in_seconds=10,
135201
)
136202

137203

tests_async/oauth2/test_id_token.py

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,30 @@ async def test_verify_token(_fetch_certs, decode):
7171
mock.sentinel.request, sync_id_token._GOOGLE_OAUTH2_CERTS_URL
7272
)
7373
decode.assert_called_once_with(
74-
mock.sentinel.token, certs=_fetch_certs.return_value, audience=None
74+
mock.sentinel.token,
75+
certs=_fetch_certs.return_value,
76+
audience=None,
77+
clock_skew_in_seconds=0,
78+
)
79+
80+
81+
@mock.patch("google.auth.jwt.decode", autospec=True)
82+
@mock.patch("google.oauth2._id_token_async._fetch_certs", autospec=True)
83+
@pytest.mark.asyncio
84+
async def test_verify_token_clock_skew(_fetch_certs, decode):
85+
result = await id_token.verify_token(
86+
mock.sentinel.token, mock.sentinel.request, clock_skew_in_seconds=10
87+
)
88+
89+
assert result == decode.return_value
90+
_fetch_certs.assert_called_once_with(
91+
mock.sentinel.request, sync_id_token._GOOGLE_OAUTH2_CERTS_URL
92+
)
93+
decode.assert_called_once_with(
94+
mock.sentinel.token,
95+
certs=_fetch_certs.return_value,
96+
audience=None,
97+
clock_skew_in_seconds=10,
7598
)
7699

77100

@@ -92,6 +115,7 @@ async def test_verify_token_args(_fetch_certs, decode):
92115
mock.sentinel.token,
93116
certs=_fetch_certs.return_value,
94117
audience=mock.sentinel.audience,
118+
clock_skew_in_seconds=0,
95119
)
96120

97121

@@ -109,6 +133,28 @@ async def test_verify_oauth2_token(verify_token):
109133
mock.sentinel.request,
110134
audience=mock.sentinel.audience,
111135
certs_url=sync_id_token._GOOGLE_OAUTH2_CERTS_URL,
136+
clock_skew_in_seconds=0,
137+
)
138+
139+
140+
@mock.patch("google.oauth2._id_token_async.verify_token", autospec=True)
141+
@pytest.mark.asyncio
142+
async def test_verify_oauth2_token_clock_skew(verify_token):
143+
verify_token.return_value = {"iss": "accounts.google.com"}
144+
result = await id_token.verify_oauth2_token(
145+
mock.sentinel.token,
146+
mock.sentinel.request,
147+
audience=mock.sentinel.audience,
148+
clock_skew_in_seconds=10,
149+
)
150+
151+
assert result == verify_token.return_value
152+
verify_token.assert_called_once_with(
153+
mock.sentinel.token,
154+
mock.sentinel.request,
155+
audience=mock.sentinel.audience,
156+
certs_url=sync_id_token._GOOGLE_OAUTH2_CERTS_URL,
157+
clock_skew_in_seconds=10,
112158
)
113159

114160

@@ -136,6 +182,27 @@ async def test_verify_firebase_token(verify_token):
136182
mock.sentinel.request,
137183
audience=mock.sentinel.audience,
138184
certs_url=sync_id_token._GOOGLE_APIS_CERTS_URL,
185+
clock_skew_in_seconds=0,
186+
)
187+
188+
189+
@mock.patch("google.oauth2._id_token_async.verify_token", autospec=True)
190+
@pytest.mark.asyncio
191+
async def test_verify_firebase_token_clock_skew(verify_token):
192+
result = await id_token.verify_firebase_token(
193+
mock.sentinel.token,
194+
mock.sentinel.request,
195+
audience=mock.sentinel.audience,
196+
clock_skew_in_seconds=10,
197+
)
198+
199+
assert result == verify_token.return_value
200+
verify_token.assert_called_once_with(
201+
mock.sentinel.token,
202+
mock.sentinel.request,
203+
audience=mock.sentinel.audience,
204+
certs_url=sync_id_token._GOOGLE_APIS_CERTS_URL,
205+
clock_skew_in_seconds=10,
139206
)
140207

141208

0 commit comments

Comments
 (0)