blob: c556397aaaac1902cad54c622e3959e4434a8965 [file] [log] [blame]
# Copyright 2021 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Cross-language tests for JWT validation.
These tests test the non-cryptographic JWT validation. The tokens are MACed
with the same key and the MAC is always valid. We test how the validation
handles weird headers or payloads.
"""
# Placeholder for import for type annotations
import base64
import datetime
from typing import Text
from absl.testing import absltest
from absl.testing import parameterized
import tink
from tink import cleartext_keyset_handle
from tink import mac
from tink.proto import common_pb2
from tink.proto import hmac_pb2
from tink.proto import jwt_hmac_pb2
from tink.proto import tink_pb2
from tink import jwt
from tink.jwt import _jwt_format
from util import testing_servers
SUPPORTED_LANGUAGES = testing_servers.SUPPORTED_LANGUAGES_BY_PRIMITIVE['jwt']
# Example from https://tools.ietf.org/html/rfc7519#section-3.1
EXAMPLE_TOKEN = ('eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.'
'eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQo'
'gImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.'
'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk')
KEY_VALUE = (b'AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-'
b'1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow==')
KEYSET = None
MAC = None
EMPTY_VALIDATOR = jwt.new_validator()
def _keyset() -> bytes:
jwt_hmac_key = jwt_hmac_pb2.JwtHmacKey(
version=0,
algorithm=jwt_hmac_pb2.HS256,
key_value=base64.urlsafe_b64decode(KEY_VALUE))
keyset = tink_pb2.Keyset()
key = keyset.key.add()
key.key_data.type_url = ('type.googleapis.com/google.crypto.tink.JwtHmacKey')
key.key_data.value = jwt_hmac_key.SerializeToString()
key.key_data.key_material_type = tink_pb2.KeyData.SYMMETRIC
key.status = tink_pb2.ENABLED
key.key_id = 123
key.output_prefix_type = tink_pb2.RAW
keyset.primary_key_id = 123
return keyset.SerializeToString()
def _mac() -> mac.Mac:
hmac_key = hmac_pb2.HmacKey(
version=0, key_value=base64.urlsafe_b64decode(KEY_VALUE))
hmac_key.params.hash = common_pb2.SHA256
hmac_key.params.tag_size = 32
keyset = tink_pb2.Keyset()
key = keyset.key.add()
key.key_data.type_url = ('type.googleapis.com/google.crypto.tink.HmacKey')
key.key_data.value = hmac_key.SerializeToString()
key.key_data.key_material_type = tink_pb2.KeyData.SYMMETRIC
key.status = tink_pb2.ENABLED
key.key_id = 123
key.output_prefix_type = tink_pb2.RAW
keyset.primary_key_id = 123
keyset_handle = cleartext_keyset_handle.from_keyset(keyset)
return keyset_handle.primitive(mac.Mac)
def setUpModule():
global KEYSET, MAC
jwt.register_jwt_mac()
mac.register()
testing_servers.start('jwt')
KEYSET = _keyset()
MAC = _mac()
def tearDownModule():
testing_servers.stop()
def generate_token_from_bytes(header: bytes, payload: bytes) -> Text:
"""Generates tokens from bytes with valid MACs."""
unsigned_compact = (
_jwt_format._base64_encode(header) + b'.' +
_jwt_format._base64_encode(payload))
mac_value = MAC.compute_mac(unsigned_compact)
return (unsigned_compact + b'.' +
_jwt_format.encode_signature(mac_value)).decode('utf8')
def generate_token(header: Text, payload: Text) -> Text:
"""Generates tokens with valid MACs."""
return generate_token_from_bytes(
header.encode('utf8'), payload.encode('utf8'))
class JwtTest(parameterized.TestCase):
def test_genenerate_token_generates_example(self):
token = generate_token(
'{"typ":"JWT",\r\n "alg":"HS256"}',
'{"iss":"joe",\r\n "exp":1300819380,\r\n '
'"http://example.com/is_root":true}')
self.assertEqual(token, EXAMPLE_TOKEN)
@parameterized.parameters(SUPPORTED_LANGUAGES)
def test_verify_valid(self, lang):
token = generate_token('{"alg":"HS256"}', '{"iss":"joe"}')
jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
verified_jwt = jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
self.assertEqual(verified_jwt.issuer(), 'joe')
@parameterized.parameters(SUPPORTED_LANGUAGES)
def test_verify_unknown_header_valid(self, lang):
token = generate_token('{"alg":"HS256", "unknown":{"a":"b"}}',
'{"iss":"joe"}')
jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
verified_jwt = jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
self.assertEqual(verified_jwt.issuer(), 'joe')
@parameterized.parameters(SUPPORTED_LANGUAGES)
def test_verify_empty_crit_header_invalid(self, lang):
# See https://tools.ietf.org/html/rfc7515#section-4.1.11
token = generate_token('{"alg":"HS256", "crit":[]}', '{"iss":"joe"}')
jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
with self.assertRaises(tink.TinkError):
jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
@parameterized.parameters(SUPPORTED_LANGUAGES)
def test_verify_nonempty_crit_header_invalid(self, lang):
# See https://tools.ietf.org/html/rfc7515#section-4.1.11
token = generate_token(
'{"alg":"HS256","crit":["http://example.invalid/UNDEFINED"],'
'"http://example.invalid/UNDEFINED":true}', '{"iss":"joe"}')
jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
with self.assertRaises(tink.TinkError):
jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
@parameterized.parameters(SUPPORTED_LANGUAGES)
def test_verify_ignores_typ_header(self, lang):
token = generate_token('{"typ":"unknown", "alg":"HS256"}', '{"iss":"joe"}')
jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
@parameterized.parameters(SUPPORTED_LANGUAGES)
def test_verify_expiration(self, lang):
token = generate_token('{"alg":"HS256"}', '{"iss":"joe", "exp":1234}')
jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
# same time is expired.
val1 = jwt.new_validator(
fixed_now=datetime.datetime.fromtimestamp(1234, datetime.timezone.utc))
with self.assertRaises(tink.TinkError):
jwt_mac.verify_mac_and_decode(token, val1)
# a fraction of a second before is fine
val2 = jwt.new_validator(
fixed_now=datetime.datetime.fromtimestamp(1233.75,
datetime.timezone.utc))
jwt_mac.verify_mac_and_decode(token, val2)
# 3 seconds too late with 3 seconds clock skew is expired.
val4 = jwt.new_validator(
fixed_now=datetime.datetime.fromtimestamp(1237, datetime.timezone.utc),
clock_skew=datetime.timedelta(seconds=3))
with self.assertRaises(tink.TinkError):
jwt_mac.verify_mac_and_decode(token, val4)
# 2 seconds too late with 3 seconds clock skew is fine.
val3 = jwt.new_validator(
fixed_now=datetime.datetime.fromtimestamp(1236, datetime.timezone.utc),
clock_skew=datetime.timedelta(seconds=3))
jwt_mac.verify_mac_and_decode(token, val3)
@parameterized.parameters(SUPPORTED_LANGUAGES)
def test_verify_float_expiration(self, lang):
token = generate_token('{"alg":"HS256"}', '{"iss":"joe", "exp":1234.5}')
jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
# same time is expired.
val1 = jwt.new_validator(
fixed_now=datetime.datetime.fromtimestamp(1234.5,
datetime.timezone.utc))
with self.assertRaises(tink.TinkError):
jwt_mac.verify_mac_and_decode(token, val1)
# a fraction of a second after is expired.
val1 = jwt.new_validator(
fixed_now=datetime.datetime.fromtimestamp(1234.75,
datetime.timezone.utc))
with self.assertRaises(tink.TinkError):
jwt_mac.verify_mac_and_decode(token, val1)
# a fraction of a second before is fine
val2 = jwt.new_validator(
fixed_now=datetime.datetime.fromtimestamp(1234.25,
datetime.timezone.utc))
jwt_mac.verify_mac_and_decode(token, val2)
@parameterized.parameters(SUPPORTED_LANGUAGES)
def test_exp_expiration_is_fine(self, lang):
token = generate_token('{"alg":"HS256"}', '{"exp":1e10}')
jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
@parameterized.parameters(SUPPORTED_LANGUAGES)
def test_large_expiration_is_fine(self, lang):
token = generate_token('{"alg":"HS256"}', '{"exp":253402300799}')
jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
@parameterized.parameters(SUPPORTED_LANGUAGES)
def test_too_large_expiration_is_invalid(self, lang):
token = generate_token('{"alg":"HS256"}', '{"exp":253402300800}')
jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
with self.assertRaises(tink.TinkError):
jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
@parameterized.parameters(SUPPORTED_LANGUAGES)
def test_way_too_large_expiration_is_invalid(self, lang):
token = generate_token('{"alg":"HS256"}', '{"exp":1e30}')
jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
with self.assertRaises(tink.TinkError):
jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
@parameterized.parameters(SUPPORTED_LANGUAGES)
def test_infinity_expiration_is_invalid(self, lang):
token = generate_token('{"alg":"HS256"}', '{"iss":"joe", "exp":Infinity}')
jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
with self.assertRaises(tink.TinkError):
jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
@parameterized.parameters(SUPPORTED_LANGUAGES)
def test_verify_not_before(self, lang):
token = generate_token('{"alg":"HS256"}', '{"iss":"joe", "nbf":1234}')
jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
# same time as nbf fine.
val1 = jwt.new_validator(
fixed_now=datetime.datetime.fromtimestamp(1234, datetime.timezone.utc))
jwt_mac.verify_mac_and_decode(token, val1)
# one second before is not yet valid
val2 = jwt.new_validator(
fixed_now=datetime.datetime.fromtimestamp(1233, datetime.timezone.utc))
with self.assertRaises(tink.TinkError):
jwt_mac.verify_mac_and_decode(token, val2)
# 3 seconds too early with 3 seconds clock skew is fine
val4 = jwt.new_validator(
fixed_now=datetime.datetime.fromtimestamp(1231, datetime.timezone.utc),
clock_skew=datetime.timedelta(seconds=3))
jwt_mac.verify_mac_and_decode(token, val4)
# 3 seconds too late with 2 seconds clock skew is fine.
val3 = jwt.new_validator(
fixed_now=datetime.datetime.fromtimestamp(1231, datetime.timezone.utc),
clock_skew=datetime.timedelta(seconds=2))
with self.assertRaises(tink.TinkError):
jwt_mac.verify_mac_and_decode(token, val3)
@parameterized.parameters(SUPPORTED_LANGUAGES)
def test_verify_float_not_before(self, lang):
token = generate_token('{"alg":"HS256"}', '{"iss":"joe", "nbf":1234.5}')
jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
val1 = jwt.new_validator(
fixed_now=datetime.datetime.fromtimestamp(1234, datetime.timezone.utc))
with self.assertRaises(tink.TinkError):
jwt_mac.verify_mac_and_decode(token, val1)
val2 = jwt.new_validator(
fixed_now=datetime.datetime.fromtimestamp(1235, datetime.timezone.utc))
jwt_mac.verify_mac_and_decode(token, val2)
@parameterized.parameters(SUPPORTED_LANGUAGES)
def test_verify_issuer(self, lang):
token = generate_token('{"alg":"HS256"}', '{"iss":"joe"}')
jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
val1 = jwt.new_validator(issuer='joe')
jwt_mac.verify_mac_and_decode(token, val1)
val2 = EMPTY_VALIDATOR
jwt_mac.verify_mac_and_decode(token, val2)
val3 = jwt.new_validator(issuer='Joe')
with self.assertRaises(tink.TinkError):
jwt_mac.verify_mac_and_decode(token, val3)
val4 = jwt.new_validator(issuer='joe ')
with self.assertRaises(tink.TinkError):
jwt_mac.verify_mac_and_decode(token, val4)
@parameterized.parameters(SUPPORTED_LANGUAGES)
def test_verify_empty_string_issuer(self, lang):
token = generate_token('{"alg":"HS256"}', '{"iss":""}')
jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
jwt_mac.verify_mac_and_decode(token, jwt.new_validator(issuer=''))
@parameterized.parameters(SUPPORTED_LANGUAGES)
def test_verify_issuer_with_wrong_type(self, lang):
token = generate_token('{"alg":"HS256"}', '{"iss":123}')
jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
with self.assertRaises(tink.TinkError):
jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
@parameterized.parameters(SUPPORTED_LANGUAGES)
def test_verify_invalid_utf8_in_header(self, lang):
token = generate_token_from_bytes(b'{"alg":"HS256", "a":"\xc2"}',
b'{"iss":"joe"}')
jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
with self.assertRaises(tink.TinkError):
jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
@parameterized.parameters(SUPPORTED_LANGUAGES)
def test_verify_invalid_utf8_in_payload(self, lang):
token = generate_token_from_bytes(b'{"alg":"HS256"}', b'{"iss":"joe\xc2"}')
jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
with self.assertRaises(tink.TinkError):
jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
@parameterized.parameters(SUPPORTED_LANGUAGES)
def test_verify_with_utf16_surrogate_in_payload(self, lang):
# The JSON string contains the G clef character (U+1D11E) in UTF8.
token = generate_token_from_bytes(b'{"alg":"HS256"}',
b'{"iss":"\xF0\x9D\x84\x9E"}')
jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
token = jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
self.assertEqual(token.issuer(), u'\U0001d11e')
@parameterized.parameters(SUPPORTED_LANGUAGES)
def test_verify_with_json_escaped_utf16_surrogate_in_payload(self, lang):
# The JSON string contains "\uD834\uDD1E", which should decode to
# the G clef character (U+1D11E).
token = generate_token('{"alg":"HS256"}', '{"iss":"\\uD834\\uDD1E"}')
jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
token = jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
self.assertEqual(token.issuer(), u'\U0001d11e')
@parameterized.parameters(SUPPORTED_LANGUAGES)
def test_verify_with_invalid_json_escaped_utf16_in_payload(self, lang):
# The JSON string contains "\uD834", which gets decoded into an invalid
# UTF16 character.
token = generate_token('{"alg":"HS256"}', '{"iss":"\\uD834"}')
jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
with self.assertRaises(tink.TinkError):
jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
@parameterized.parameters(SUPPORTED_LANGUAGES)
def test_verify_subject(self, lang):
token = generate_token('{"alg":"HS256"}', '{"sub":"joe"}')
jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
val1 = jwt.new_validator(subject='joe')
jwt_mac.verify_mac_and_decode(token, val1)
val2 = EMPTY_VALIDATOR
jwt_mac.verify_mac_and_decode(token, val2)
val3 = jwt.new_validator(subject='Joe')
with self.assertRaises(tink.TinkError):
jwt_mac.verify_mac_and_decode(token, val3)
val4 = jwt.new_validator(subject='joe ')
with self.assertRaises(tink.TinkError):
jwt_mac.verify_mac_and_decode(token, val4)
@parameterized.parameters(SUPPORTED_LANGUAGES)
def test_verify_empty_string_subject(self, lang):
token = generate_token('{"alg":"HS256"}', '{"sub":""}')
jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
jwt_mac.verify_mac_and_decode(token, jwt.new_validator(subject=''))
@parameterized.parameters(SUPPORTED_LANGUAGES)
def test_verify_subject_with_wrong_type(self, lang):
token = generate_token('{"alg":"HS256"}', '{"sub":123}')
jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
with self.assertRaises(tink.TinkError):
jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
@parameterized.parameters(SUPPORTED_LANGUAGES)
def test_verify_audience(self, lang):
token = generate_token('{"alg":"HS256"}', '{"aud":["joe", "jane"]}')
jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
val1 = jwt.new_validator(audience='joe')
jwt_mac.verify_mac_and_decode(token, val1)
val2 = jwt.new_validator(audience='jane')
jwt_mac.verify_mac_and_decode(token, val2)
val3 = EMPTY_VALIDATOR
with self.assertRaises(tink.TinkError):
jwt_mac.verify_mac_and_decode(token, val3)
val4 = jwt.new_validator(audience='Joe')
with self.assertRaises(tink.TinkError):
jwt_mac.verify_mac_and_decode(token, val4)
val5 = jwt.new_validator(audience='jane ')
with self.assertRaises(tink.TinkError):
jwt_mac.verify_mac_and_decode(token, val5)
@parameterized.parameters(SUPPORTED_LANGUAGES)
def test_verify_audience_string(self, lang):
token = generate_token('{"alg":"HS256"}', '{"aud":"joe"}')
jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
val1 = jwt.new_validator(audience='joe')
jwt_mac.verify_mac_and_decode(token, val1)
val3 = EMPTY_VALIDATOR
with self.assertRaises(tink.TinkError):
jwt_mac.verify_mac_and_decode(token, val3)
@parameterized.parameters(SUPPORTED_LANGUAGES)
def test_verify_audiences_with_wrong_type(self, lang):
token = generate_token('{"alg":"HS256"}', '{"aud":["joe", 123]}')
jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
with self.assertRaises(tink.TinkError):
jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
@parameterized.parameters(SUPPORTED_LANGUAGES)
def test_verify_token_with_empty_audiences(self, lang):
token = generate_token('{"alg":"HS256"}', '{"aud":[]}')
jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
with self.assertRaises(tink.TinkError):
jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
@parameterized.parameters(SUPPORTED_LANGUAGES)
def test_verify_token_with_many_recursions(self, lang):
num_recursions = 10
payload = ('{"a":' * num_recursions) + '""' + ('}' * num_recursions)
token = generate_token('{"alg":"HS256"}', payload)
jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
@parameterized.parameters(SUPPORTED_LANGUAGES)
def test_verify_token_with_too_many_recursions_fails(self, lang):
# num_recursions has been chosen such that parsing of this token fails
# in all languages. We want to make sure that the algorithm does not
# hang or crash in this case, but only returns a parsing error.
num_recursions = 10000
payload = ('{"a":' * num_recursions) + '""' + ('}' * num_recursions)
token = generate_token('{"alg":"HS256"}', payload)
jwt_mac = testing_servers.jwt_mac(lang, KEYSET)
with self.assertRaises(tink.TinkError):
jwt_mac.verify_mac_and_decode(token, EMPTY_VALIDATOR)
if __name__ == '__main__':
absltest.main()