# 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
"""Tests for tink.python.tink.jwt._jwt_validator."""

import datetime

from absl.testing import absltest
from tink import jwt
from tink.jwt import _jwt_validator


class JwtValidatorTest(absltest.TestCase):

  def test_validator_getters(self):
    fixed_now = datetime.datetime.fromtimestamp(12345, datetime.timezone.utc)
    clock_skew = datetime.timedelta(minutes=1)
    validator = jwt.new_validator(
        expected_type_header='type_header',
        expected_issuer='issuer',
        expected_audience='audience',
        fixed_now=fixed_now,
        clock_skew=clock_skew)
    self.assertTrue(validator.has_expected_type_header())
    self.assertEqual(validator.expected_type_header(), 'type_header')
    self.assertTrue(validator.has_expected_issuer())
    self.assertEqual(validator.expected_issuer(), 'issuer')
    self.assertTrue(validator.has_expected_audience())
    self.assertEqual(validator.expected_audience(), 'audience')
    self.assertFalse(validator.allow_missing_expiration())
    self.assertFalse(validator.ignore_issuer())
    self.assertFalse(validator.ignore_audiences())
    self.assertTrue(validator.has_fixed_now())
    self.assertEqual(validator.fixed_now(), fixed_now)
    self.assertEqual(validator.clock_skew(), clock_skew)

  def test_validator_ignore_getters(self):
    validator = jwt.new_validator(
        allow_missing_expiration=True,
        ignore_type_header=True,
        ignore_issuer=True,
        ignore_audiences=True)
    self.assertTrue(validator.allow_missing_expiration())
    self.assertTrue(validator.ignore_type_header())
    self.assertTrue(validator.ignore_issuer())
    self.assertTrue(validator.ignore_audiences())

  def test_empty_validator_getters(self):
    validator = jwt.new_validator()
    self.assertFalse(validator.has_expected_type_header())
    self.assertFalse(validator.has_expected_issuer())
    self.assertFalse(validator.has_expected_audience())
    self.assertFalse(validator.has_fixed_now())
    self.assertFalse(validator.clock_skew(), datetime.timedelta())

  def test_too_much_clock_skew(self):
    with self.assertRaises(ValueError):
      jwt.new_validator(clock_skew=datetime.timedelta(minutes=20))

  def test_validate_expired_fails(self):
    expired = (datetime.datetime.now(tz=datetime.timezone.utc)
               - datetime.timedelta(minutes=1))
    token = jwt.new_raw_jwt(expiration=expired)
    validator = jwt.new_validator()
    with self.assertRaises(jwt.JwtInvalidError):
      _jwt_validator.validate(validator, token)

  def test_validate_not_expired_success(self):
    still_valid = (datetime.datetime.now(tz=datetime.timezone.utc)
                   + datetime.timedelta(minutes=1))
    token = jwt.new_raw_jwt(expiration=still_valid)
    validator = jwt.new_validator()
    _jwt_validator.validate(validator, token)

  def test_validate_token_that_expires_now_fails(self):
    now = datetime.datetime.fromtimestamp(1234.0, tz=datetime.timezone.utc)
    token = jwt.new_raw_jwt(expiration=now)
    validator = jwt.new_validator()
    with self.assertRaises(jwt.JwtInvalidError):
      _jwt_validator.validate(validator, token)

  def test_validate_recently_expired_with_clock_skew_success(self):
    recently_expired = (datetime.datetime.now(tz=datetime.timezone.utc)
                        - datetime.timedelta(minutes=1))
    token = jwt.new_raw_jwt(expiration=recently_expired)
    validator = jwt.new_validator(clock_skew=datetime.timedelta(minutes=2))
    # because of clock_skew, the recently expired token is valid
    _jwt_validator.validate(validator, token)

  def test_validate_not_before_in_the_future_fails(self):
    in_the_future = (datetime.datetime.now(tz=datetime.timezone.utc)
                     + datetime.timedelta(minutes=1))
    token = jwt.new_raw_jwt(not_before=in_the_future, without_expiration=True)
    validator = jwt.new_validator(allow_missing_expiration=True)
    with self.assertRaises(jwt.JwtInvalidError):
      _jwt_validator.validate(validator, token)

  def test_validate_not_before_in_the_past_success(self):
    in_the_past = (datetime.datetime.now(tz=datetime.timezone.utc)
                   - datetime.timedelta(minutes=1))
    token = jwt.new_raw_jwt(not_before=in_the_past, without_expiration=True)
    validator = jwt.new_validator(allow_missing_expiration=True)
    _jwt_validator.validate(validator, token)

  def test_validate_not_before_is_now_success(self):
    now = datetime.datetime.fromtimestamp(12345, datetime.timezone.utc)
    token = jwt.new_raw_jwt(not_before=now, without_expiration=True)
    validator = jwt.new_validator(allow_missing_expiration=True)
    _jwt_validator.validate(validator, token)

  def test_validate_not_before_almost_reached_with_clock_skew_success(self):
    in_one_minute = (datetime.datetime.now(tz=datetime.timezone.utc)
                     + datetime.timedelta(minutes=1))
    token = jwt.new_raw_jwt(not_before=in_one_minute, without_expiration=True)
    validator = jwt.new_validator(
        allow_missing_expiration=True, clock_skew=datetime.timedelta(minutes=2))
    _jwt_validator.validate(validator, token)

  def test_validate_issued_at(self):
    in_one_minute = (datetime.datetime.now(tz=datetime.timezone.utc)
                     + datetime.timedelta(minutes=1))
    one_minute_ago = (datetime.datetime.now(tz=datetime.timezone.utc)
                      - datetime.timedelta(minutes=1))
    token_with_issued_at_in_the_future = jwt.new_raw_jwt(
        issued_at=in_one_minute, without_expiration=True)
    token_with_issued_at_in_the_past = jwt.new_raw_jwt(
        issued_at=one_minute_ago, without_expiration=True)
    token_without_issued_at = jwt.new_raw_jwt(without_expiration=True)

    validator = jwt.new_validator(allow_missing_expiration=True)
    _jwt_validator.validate(validator, token_with_issued_at_in_the_future)
    _jwt_validator.validate(validator, token_with_issued_at_in_the_past)
    _jwt_validator.validate(validator, token_without_issued_at)

    issued_at_validator = jwt.new_validator(
        expect_issued_in_the_past=True, allow_missing_expiration=True)
    with self.assertRaises(jwt.JwtInvalidError):
      _jwt_validator.validate(issued_at_validator,
                              token_with_issued_at_in_the_future)
    _jwt_validator.validate(issued_at_validator,
                            token_with_issued_at_in_the_past)
    with self.assertRaises(jwt.JwtInvalidError):
      _jwt_validator.validate(issued_at_validator, token_without_issued_at)

  def test_validate_issued_at_with_clock_skew(self):
    in_one_minute = (datetime.datetime.now(tz=datetime.timezone.utc)
                     + datetime.timedelta(minutes=1))
    token_one_minute_in_the_future = jwt.new_raw_jwt(
        issued_at=in_one_minute, without_expiration=True)

    validator_without_clock_skew = jwt.new_validator(
        expect_issued_in_the_past=True, allow_missing_expiration=True)
    with self.assertRaises(jwt.JwtInvalidError):
      _jwt_validator.validate(validator_without_clock_skew,
                              token_one_minute_in_the_future)

    validator_with_clock_skew = jwt.new_validator(
        expect_issued_in_the_past=True,
        allow_missing_expiration=True,
        clock_skew=datetime.timedelta(minutes=2))
    _jwt_validator.validate(validator_with_clock_skew,
                            token_one_minute_in_the_future)

  def test_requires_type_header_but_no_type_header_set_fails(self):
    token = jwt.new_raw_jwt(without_expiration=True)
    validator = jwt.new_validator(
        expected_type_header='type_header', allow_missing_expiration=True)
    with self.assertRaises(jwt.JwtInvalidError):
      _jwt_validator.validate(validator, token)

  def test_invalid_type_header_fails(self):
    token = jwt.new_raw_jwt(type_header='unknown', without_expiration=True)
    validator = jwt.new_validator(
        expected_type_header='type_header', allow_missing_expiration=True)
    with self.assertRaises(jwt.JwtInvalidError):
      _jwt_validator.validate(validator, token)

  def test_correct_type_header_success(self):
    token = jwt.new_raw_jwt(type_header='type_header', without_expiration=True)
    validator = jwt.new_validator(
        expected_type_header='type_header', allow_missing_expiration=True)
    _jwt_validator.validate(validator, token)

  def test_type_header_in_token_but_not_in_validator_fails(self):
    validator = jwt.new_validator(allow_missing_expiration=True)
    token_with_type_header = jwt.new_raw_jwt(
        type_header='type_header', without_expiration=True)
    with self.assertRaises(jwt.JwtInvalidError):
      _jwt_validator.validate(validator, token_with_type_header)

  def test_ignore_type_header_success(self):
    validator = jwt.new_validator(
        ignore_type_header=True, allow_missing_expiration=True)
    token_without_type_header = jwt.new_raw_jwt(without_expiration=True)
    _jwt_validator.validate(validator, token_without_type_header)
    token_with_type_header = jwt.new_raw_jwt(
        type_header='type_header', without_expiration=True)
    _jwt_validator.validate(validator, token_with_type_header)

  def test_requires_issuer_but_no_issuer_set_fails(self):
    token = jwt.new_raw_jwt(without_expiration=True)
    validator = jwt.new_validator(
        expected_issuer='issuer', allow_missing_expiration=True)
    with self.assertRaises(jwt.JwtInvalidError):
      _jwt_validator.validate(validator, token)

  def test_invalid_issuer_fails(self):
    token = jwt.new_raw_jwt(issuer='unknown', without_expiration=True)
    validator = jwt.new_validator(
        expected_issuer='issuer', allow_missing_expiration=True)
    with self.assertRaises(jwt.JwtInvalidError):
      _jwt_validator.validate(validator, token)

  def test_correct_issuer_success(self):
    token = jwt.new_raw_jwt(issuer='issuer', without_expiration=True)
    validator = jwt.new_validator(
        expected_issuer='issuer', allow_missing_expiration=True)
    _jwt_validator.validate(validator, token)

  def test_issuer_in_token_but_not_in_validator_fails(self):
    validator = jwt.new_validator(allow_missing_expiration=True)
    token_with_issuer = jwt.new_raw_jwt(
        issuer='issuer', without_expiration=True)
    with self.assertRaises(jwt.JwtInvalidError):
      _jwt_validator.validate(validator, token_with_issuer)

  def test_ignore_issuer_success(self):
    validator = jwt.new_validator(
        ignore_issuer=True, allow_missing_expiration=True)
    token_without_issuer = jwt.new_raw_jwt(without_expiration=True)
    _jwt_validator.validate(validator, token_without_issuer)
    token_with_issuer = jwt.new_raw_jwt(
        issuer='issuer', without_expiration=True)
    _jwt_validator.validate(validator, token_with_issuer)

  def test_requires_audience_but_no_audience_set_fails(self):
    token = jwt.new_raw_jwt(without_expiration=True)
    validator = jwt.new_validator(
        expected_audience='audience', allow_missing_expiration=True)
    with self.assertRaises(jwt.JwtInvalidError):
      _jwt_validator.validate(validator, token)

  def test_wrong_audience_fails(self):
    token = jwt.new_raw_jwt(audiences=['unknown'], without_expiration=True)
    validator = jwt.new_validator(
        expected_audience='audience', allow_missing_expiration=True)
    with self.assertRaises(jwt.JwtInvalidError):
      _jwt_validator.validate(validator, token)

  def test_correct_audience_success(self):
    token = jwt.new_raw_jwt(audiences=['you', 'me'], without_expiration=True)
    validator = jwt.new_validator(
        expected_audience='me', allow_missing_expiration=True)
    _jwt_validator.validate(validator, token)

  def test_audience_in_token_but_not_in_validator_fails(self):
    validator = jwt.new_validator(allow_missing_expiration=True)
    token_with_audience = jwt.new_raw_jwt(
        audiences=['audience'], without_expiration=True)
    with self.assertRaises(jwt.JwtInvalidError):
      _jwt_validator.validate(validator, token_with_audience)

  def test_no_audience_success(self):
    validator = jwt.new_validator(allow_missing_expiration=True)
    token = jwt.new_raw_jwt(without_expiration=True)
    _jwt_validator.validate(validator, token)

  def test_ignore_audiences_success(self):
    validator = jwt.new_validator(
        ignore_audiences=True, allow_missing_expiration=True)
    token_without_audience = jwt.new_raw_jwt(without_expiration=True)
    _jwt_validator.validate(validator, token_without_audience)
    token_with_audience = jwt.new_raw_jwt(
        audiences=['audience'], without_expiration=True)
    _jwt_validator.validate(validator, token_with_audience)

  def test_validate_with_fixed_now_expired_fails(self):
    in_two_minutes = (
        datetime.datetime.now(tz=datetime.timezone.utc) +
        datetime.timedelta(minutes=2))
    in_one_minute = in_two_minutes - datetime.timedelta(minutes=1)
    token = jwt.new_raw_jwt(expiration=in_one_minute)
    validator = jwt.new_validator(fixed_now=in_two_minutes)
    with self.assertRaises(jwt.JwtInvalidError):
      _jwt_validator.validate(validator, token)

  def test_validate_with_fixed_now_not_yet_valid_fails(self):
    two_minutes_ago = (
        datetime.datetime.now(tz=datetime.timezone.utc) -
        datetime.timedelta(minutes=2))
    one_minute_ago = two_minutes_ago + datetime.timedelta(minutes=1)
    token = jwt.new_raw_jwt(not_before=one_minute_ago, without_expiration=True)
    validator = jwt.new_validator(fixed_now=two_minutes_ago)
    with self.assertRaises(jwt.JwtInvalidError):
      _jwt_validator.validate(validator, token)

  def test_validate_with_fixed_now_valid_success(self):
    fixed_now = datetime.datetime.fromtimestamp(12345, datetime.timezone.utc)
    validator = jwt.new_validator(fixed_now=fixed_now)
    expiration = fixed_now + datetime.timedelta(minutes=1)
    not_before = fixed_now - datetime.timedelta(minutes=1)
    token = jwt.new_raw_jwt(expiration=expiration, not_before=not_before)
    _jwt_validator.validate(validator, token)

  def test_invalid_clock_skew_fail(self):
    with self.assertRaises(ValueError):
      jwt.new_validator(clock_skew=datetime.timedelta(minutes=1000))

  def test_fixed_now_without_timezone_fail(self):
    with self.assertRaises(ValueError):
      jwt.new_validator(fixed_now=datetime.datetime.fromtimestamp(12345))


if __name__ == '__main__':
  absltest.main()
