Skip to content

Commit 7e97e86

Browse files
committed
e2e functional tests
1 parent 6cc8e1c commit 7e97e86

1 file changed

Lines changed: 122 additions & 4 deletions

File tree

tools/integration_tests/tokenserver/test_e2e.py

Lines changed: 122 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
# You can obtain one at http://mozilla.org/MPL/2.0/.
44
"""End-to-end integration tests for the tokenserver."""
55

6-
from base64 import urlsafe_b64decode
76
import hmac
87
import json
98
import jwt
@@ -12,16 +11,22 @@
1211
import time
1312
import tokenlib
1413
import unittest
14+
from base64 import urlsafe_b64decode
15+
from hashlib import sha256
1516

17+
from cryptography.hazmat.backends import default_backend
1618
from cryptography.hazmat.primitives import serialization
1719
from cryptography.hazmat.primitives.asymmetric import rsa
18-
from cryptography.hazmat.backends import default_backend
1920
from fxa.core import Client
20-
from fxa.oauth import Client as OAuthClient
2121
from fxa.errors import ClientError, ServerError
22+
from fxa.oauth import Client as OAuthClient
2223
from fxa.tests.utils import TestEmailAccount
23-
from hashlib import sha256
2424

25+
from integration_tests.tokenserver.conftest import (
26+
FXA_METRICS_HASH_SECRET,
27+
TOKEN_SIGNING_SECRET,
28+
unsafe_parse_token,
29+
)
2530
from integration_tests.tokenserver.test_support import TestCase
2631

2732
# This is the client ID used for Firefox Desktop. The FxA team confirmed that
@@ -219,3 +224,116 @@ def test_valid_oauth_request(self):
219224
self.assertNotEqual(token["hashed_fxa_uid"], token["fxa_uid"])
220225
self.assertEqual(token["hashed_fxa_uid"], res.json["hashed_fxa_uid"])
221226
self.assertIn("hashed_device_id", token)
227+
228+
229+
# ===========================================================================
230+
# pytest-style equivalents — added alongside the class above.
231+
# Once confirmed passing in CI the TestE2e class will be removed.
232+
# fxa_auth is session-scoped (justified: real FxA account creation is a slow
233+
# network call; one account suffices for the whole session).
234+
# Fixtures are defined in tools/integration_tests/tokenserver/conftest.py.
235+
# ===========================================================================
236+
237+
238+
def _fxa_metrics_hash(value: str) -> str:
239+
"""Compute the FxA metrics hash for a given value."""
240+
hasher = hmac.new(FXA_METRICS_HASH_SECRET.encode("utf-8"), b"", sha256)
241+
hasher.update(value.encode("utf-8"))
242+
return hasher.hexdigest()
243+
244+
245+
def _get_bad_token() -> str:
246+
"""Generate a JWT signed with an untrusted key."""
247+
key = rsa.generate_private_key(
248+
backend=default_backend(), public_exponent=65537, key_size=2048
249+
)
250+
pem = key.private_bytes(
251+
encoding=serialization.Encoding.PEM,
252+
format=serialization.PrivateFormat.TraditionalOpenSSL,
253+
encryption_algorithm=serialization.NoEncryption(),
254+
)
255+
claims = {"sub": "fake sub", "iat": 12345, "exp": 12345}
256+
return jwt.encode(claims, pem.decode("utf-8"), algorithm="RS256")
257+
258+
259+
def test_unauthorized_oauth_error_status(ts_ctx, fxa_auth):
260+
"""Test unauthorized oauth error status."""
261+
app = ts_ctx["app"]
262+
oauth_client = fxa_auth["oauth_client"]
263+
session = fxa_auth["session"]
264+
265+
# Totally busted auth -> generic error.
266+
headers = {
267+
"Authorization": "Unsupported-Auth-Scheme IHACKYOU",
268+
"X-KeyID": "1234-qqo",
269+
}
270+
res = app.get("/1.0/sync/1.5", headers=headers, status=401)
271+
expected_error_response = {
272+
"errors": [{"description": "Unsupported", "location": "body", "name": ""}],
273+
"status": "error",
274+
}
275+
assert res.json == expected_error_response
276+
277+
bad_token = _get_bad_token()
278+
headers = {"Authorization": f"Bearer {bad_token}", "X-KeyID": "1234-qqo"}
279+
# Bad token -> 'invalid-credentials'
280+
res = app.get("/1.0/sync/1.5", headers=headers, status=401)
281+
expected_error_response = {
282+
"errors": [{"description": "Unauthorized", "location": "body", "name": ""}],
283+
"status": "invalid-credentials",
284+
}
285+
assert res.json == expected_error_response
286+
287+
# Untrusted scopes -> 'invalid-credentials'
288+
bad_scope_token = oauth_client.authorize_token(session, "bad_scope")
289+
headers = {"Authorization": f"Bearer {bad_scope_token}", "X-KeyID": "1234-qqo"}
290+
res = app.get("/1.0/sync/1.5", headers=headers, status=401)
291+
assert res.json == expected_error_response
292+
293+
294+
def test_valid_oauth_request(ts_ctx, fxa_auth):
295+
"""Test valid oauth request."""
296+
app = ts_ctx["app"]
297+
expected_node_type = ts_ctx["expected_node_type"]
298+
session = fxa_auth["session"]
299+
oauth_token = fxa_auth["oauth_token"]
300+
301+
headers = {"Authorization": f"Bearer {oauth_token}", "X-KeyID": "1234-qqo"}
302+
# Send a valid request, allocating a new user
303+
res = app.get("/1.0/sync/1.5", headers=headers)
304+
fxa_uid = session.uid
305+
306+
# Verify the token signature using tokenlib
307+
raw = urlsafe_b64decode(res.json["id"])
308+
payload = raw[:-32]
309+
signature = raw[-32:]
310+
payload_str = payload.decode("utf-8")
311+
payload_dict = json.loads(payload_str)
312+
# The `id` payload should include a field indicating the origin of the token
313+
assert payload_dict["tokenserver_origin"] == "rust"
314+
signing_secret = TOKEN_SIGNING_SECRET
315+
tm = tokenlib.TokenManager(secret=signing_secret)
316+
expected_signature = tm._get_signature(payload_str.encode("utf8"))
317+
# Using compare_digest here is good practice even in a test context
318+
assert hmac.compare_digest(expected_signature, signature)
319+
# Check that the given key is a secret derived from the hawk ID
320+
expected_secret = tokenlib.get_derived_secret(res.json["id"], secret=signing_secret)
321+
assert res.json["key"] == expected_secret
322+
# Check to make sure the remainder of the fields are valid
323+
assert res.json["api_endpoint"].startswith("https://example.com/1.5/")
324+
assert res.json["duration"] == 3600
325+
assert res.json["hashalg"] == "sha256"
326+
assert res.json["hashed_fxa_uid"] == _fxa_metrics_hash(fxa_uid)[:32]
327+
# Verify the node_type matches the syncstorage backend being tested
328+
assert res.json["node_type"] == expected_node_type
329+
# The response should have an X-Timestamp header
330+
assert "X-Timestamp" in res.headers
331+
assert int(res.headers["X-Timestamp"]) is not None
332+
token = unsafe_parse_token(res.json["id"])
333+
assert "hashed_device_id" in token
334+
assert token["uid"] == res.json["uid"]
335+
assert token["fxa_uid"] == fxa_uid
336+
assert token["fxa_kid"] == "0000000001234-qqo"
337+
assert token["hashed_fxa_uid"] != token["fxa_uid"]
338+
assert token["hashed_fxa_uid"] == res.json["hashed_fxa_uid"]
339+
assert "hashed_device_id" in token

0 commit comments

Comments
 (0)