| # Copyright 2014 Google Inc. |
| # |
| # 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. |
| |
| import base64 |
| import datetime |
| import json |
| import os |
| |
| import mock |
| import pytest |
| |
| from google.auth import _helpers |
| from google.auth import crypt |
| from google.auth import exceptions |
| from google.auth import jwt |
| |
| |
| DATA_DIR = os.path.join(os.path.dirname(__file__), 'data') |
| |
| with open(os.path.join(DATA_DIR, 'privatekey.pem'), 'rb') as fh: |
| PRIVATE_KEY_BYTES = fh.read() |
| |
| with open(os.path.join(DATA_DIR, 'public_cert.pem'), 'rb') as fh: |
| PUBLIC_CERT_BYTES = fh.read() |
| |
| with open(os.path.join(DATA_DIR, 'other_cert.pem'), 'rb') as fh: |
| OTHER_CERT_BYTES = fh.read() |
| |
| SERVICE_ACCOUNT_JSON_FILE = os.path.join(DATA_DIR, 'service_account.json') |
| |
| with open(SERVICE_ACCOUNT_JSON_FILE, 'r') as fh: |
| SERVICE_ACCOUNT_INFO = json.load(fh) |
| |
| |
| @pytest.fixture |
| def signer(): |
| return crypt.RSASigner.from_string(PRIVATE_KEY_BYTES, '1') |
| |
| |
| def test_encode_basic(signer): |
| test_payload = {'test': 'value'} |
| encoded = jwt.encode(signer, test_payload) |
| header, payload, _, _ = jwt._unverified_decode(encoded) |
| assert payload == test_payload |
| assert header == {'typ': 'JWT', 'alg': 'RS256', 'kid': signer.key_id} |
| |
| |
| def test_encode_extra_headers(signer): |
| encoded = jwt.encode(signer, {}, header={'extra': 'value'}) |
| header = jwt.decode_header(encoded) |
| assert header == { |
| 'typ': 'JWT', 'alg': 'RS256', 'kid': signer.key_id, 'extra': 'value'} |
| |
| |
| @pytest.fixture |
| def token_factory(signer): |
| def factory(claims=None, key_id=None): |
| now = _helpers.datetime_to_secs(_helpers.utcnow()) |
| payload = { |
| 'aud': '[email protected]', |
| 'iat': now, |
| 'exp': now + 300, |
| 'user': 'billy bob', |
| 'metadata': {'meta': 'data'} |
| } |
| payload.update(claims or {}) |
| |
| # False is specified to remove the signer's key id for testing |
| # headers without key ids. |
| if key_id is False: |
| signer._key_id = None |
| key_id = None |
| |
| return jwt.encode(signer, payload, key_id=key_id) |
| return factory |
| |
| |
| def test_decode_valid(token_factory): |
| payload = jwt.decode(token_factory(), certs=PUBLIC_CERT_BYTES) |
| assert payload['aud'] == '[email protected]' |
| assert payload['user'] == 'billy bob' |
| assert payload['metadata']['meta'] == 'data' |
| |
| |
| def test_decode_valid_with_audience(token_factory): |
| payload = jwt.decode( |
| token_factory(), certs=PUBLIC_CERT_BYTES, |
| audience='[email protected]') |
| assert payload['aud'] == '[email protected]' |
| assert payload['user'] == 'billy bob' |
| assert payload['metadata']['meta'] == 'data' |
| |
| |
| def test_decode_valid_unverified(token_factory): |
| payload = jwt.decode(token_factory(), certs=OTHER_CERT_BYTES, verify=False) |
| assert payload['aud'] == '[email protected]' |
| assert payload['user'] == 'billy bob' |
| assert payload['metadata']['meta'] == 'data' |
| |
| |
| def test_decode_bad_token_wrong_number_of_segments(): |
| with pytest.raises(ValueError) as excinfo: |
| jwt.decode('1.2', PUBLIC_CERT_BYTES) |
| assert excinfo.match(r'Wrong number of segments') |
| |
| |
| def test_decode_bad_token_not_base64(): |
| with pytest.raises((ValueError, TypeError)) as excinfo: |
| jwt.decode('1.2.3', PUBLIC_CERT_BYTES) |
| assert excinfo.match(r'Incorrect padding') |
| |
| |
| def test_decode_bad_token_not_json(): |
| token = b'.'.join([base64.urlsafe_b64encode(b'123!')] * 3) |
| with pytest.raises(ValueError) as excinfo: |
| jwt.decode(token, PUBLIC_CERT_BYTES) |
| assert excinfo.match(r'Can\'t parse segment') |
| |
| |
| def test_decode_bad_token_no_iat_or_exp(signer): |
| token = jwt.encode(signer, {'test': 'value'}) |
| with pytest.raises(ValueError) as excinfo: |
| jwt.decode(token, PUBLIC_CERT_BYTES) |
| assert excinfo.match(r'Token does not contain required claim') |
| |
| |
| def test_decode_bad_token_too_early(token_factory): |
| token = token_factory(claims={ |
| 'iat': _helpers.datetime_to_secs( |
| _helpers.utcnow() + datetime.timedelta(hours=1)) |
| }) |
| with pytest.raises(ValueError) as excinfo: |
| jwt.decode(token, PUBLIC_CERT_BYTES) |
| assert excinfo.match(r'Token used too early') |
| |
| |
| def test_decode_bad_token_expired(token_factory): |
| token = token_factory(claims={ |
| 'exp': _helpers.datetime_to_secs( |
| _helpers.utcnow() - datetime.timedelta(hours=1)) |
| }) |
| with pytest.raises(ValueError) as excinfo: |
| jwt.decode(token, PUBLIC_CERT_BYTES) |
| assert excinfo.match(r'Token expired') |
| |
| |
| def test_decode_bad_token_wrong_audience(token_factory): |
| token = token_factory() |
| audience = '[email protected]' |
| with pytest.raises(ValueError) as excinfo: |
| jwt.decode(token, PUBLIC_CERT_BYTES, audience=audience) |
| assert excinfo.match(r'Token has wrong audience') |
| |
| |
| def test_decode_wrong_cert(token_factory): |
| with pytest.raises(ValueError) as excinfo: |
| jwt.decode(token_factory(), OTHER_CERT_BYTES) |
| assert excinfo.match(r'Could not verify token signature') |
| |
| |
| def test_decode_multicert_bad_cert(token_factory): |
| certs = {'1': OTHER_CERT_BYTES, '2': PUBLIC_CERT_BYTES} |
| with pytest.raises(ValueError) as excinfo: |
| jwt.decode(token_factory(), certs) |
| assert excinfo.match(r'Could not verify token signature') |
| |
| |
| def test_decode_no_cert(token_factory): |
| certs = {'2': PUBLIC_CERT_BYTES} |
| with pytest.raises(ValueError) as excinfo: |
| jwt.decode(token_factory(), certs) |
| assert excinfo.match(r'Certificate for key id 1 not found') |
| |
| |
| def test_decode_no_key_id(token_factory): |
| token = token_factory(key_id=False) |
| certs = {'2': PUBLIC_CERT_BYTES} |
| payload = jwt.decode(token, certs) |
| assert payload['user'] == 'billy bob' |
| |
| |
| def test_roundtrip_explicit_key_id(token_factory): |
| token = token_factory(key_id='3') |
| certs = {'2': OTHER_CERT_BYTES, '3': PUBLIC_CERT_BYTES} |
| payload = jwt.decode(token, certs) |
| assert payload['user'] == 'billy bob' |
| |
| |
| class TestCredentials(object): |
| SERVICE_ACCOUNT_EMAIL = '[email protected]' |
| SUBJECT = 'subject' |
| AUDIENCE = 'audience' |
| ADDITIONAL_CLAIMS = {'meta': 'data'} |
| credentials = None |
| |
| @pytest.fixture(autouse=True) |
| def credentials_fixture(self, signer): |
| self.credentials = jwt.Credentials( |
| signer, self.SERVICE_ACCOUNT_EMAIL, self.SERVICE_ACCOUNT_EMAIL, |
| self.AUDIENCE) |
| |
| def test_from_service_account_info(self): |
| with open(SERVICE_ACCOUNT_JSON_FILE, 'r') as fh: |
| info = json.load(fh) |
| |
| credentials = jwt.Credentials.from_service_account_info( |
| info, audience=self.AUDIENCE) |
| |
| assert credentials._signer.key_id == info['private_key_id'] |
| assert credentials._issuer == info['client_email'] |
| assert credentials._subject == info['client_email'] |
| assert credentials._audience == self.AUDIENCE |
| |
| def test_from_service_account_info_args(self): |
| info = SERVICE_ACCOUNT_INFO.copy() |
| |
| credentials = jwt.Credentials.from_service_account_info( |
| info, subject=self.SUBJECT, audience=self.AUDIENCE, |
| additional_claims=self.ADDITIONAL_CLAIMS) |
| |
| assert credentials._signer.key_id == info['private_key_id'] |
| assert credentials._issuer == info['client_email'] |
| assert credentials._subject == self.SUBJECT |
| assert credentials._audience == self.AUDIENCE |
| assert credentials._additional_claims == self.ADDITIONAL_CLAIMS |
| |
| def test_from_service_account_file(self): |
| info = SERVICE_ACCOUNT_INFO.copy() |
| |
| credentials = jwt.Credentials.from_service_account_file( |
| SERVICE_ACCOUNT_JSON_FILE, audience=self.AUDIENCE) |
| |
| assert credentials._signer.key_id == info['private_key_id'] |
| assert credentials._issuer == info['client_email'] |
| assert credentials._subject == info['client_email'] |
| assert credentials._audience == self.AUDIENCE |
| |
| def test_from_service_account_file_args(self): |
| info = SERVICE_ACCOUNT_INFO.copy() |
| |
| credentials = jwt.Credentials.from_service_account_file( |
| SERVICE_ACCOUNT_JSON_FILE, subject=self.SUBJECT, |
| audience=self.AUDIENCE, additional_claims=self.ADDITIONAL_CLAIMS) |
| |
| assert credentials._signer.key_id == info['private_key_id'] |
| assert credentials._issuer == info['client_email'] |
| assert credentials._subject == self.SUBJECT |
| assert credentials._audience == self.AUDIENCE |
| assert credentials._additional_claims == self.ADDITIONAL_CLAIMS |
| |
| def test_from_signing_credentials(self): |
| jwt_from_signing = self.credentials.from_signing_credentials( |
| self.credentials, |
| audience=mock.sentinel.new_audience) |
| jwt_from_info = jwt.Credentials.from_service_account_info( |
| SERVICE_ACCOUNT_INFO, |
| audience=mock.sentinel.new_audience) |
| |
| assert isinstance(jwt_from_signing, jwt.Credentials) |
| assert jwt_from_signing._signer.key_id == jwt_from_info._signer.key_id |
| assert jwt_from_signing._issuer == jwt_from_info._issuer |
| assert jwt_from_signing._subject == jwt_from_info._subject |
| assert jwt_from_signing._audience == jwt_from_info._audience |
| |
| def test_default_state(self): |
| assert not self.credentials.valid |
| # Expiration hasn't been set yet |
| assert not self.credentials.expired |
| |
| def test_with_claims(self): |
| new_audience = 'new_audience' |
| new_credentials = self.credentials.with_claims( |
| audience=new_audience) |
| |
| assert new_credentials._signer == self.credentials._signer |
| assert new_credentials._issuer == self.credentials._issuer |
| assert new_credentials._subject == self.credentials._subject |
| assert new_credentials._audience == new_audience |
| assert (new_credentials._additional_claims == |
| self.credentials._additional_claims) |
| |
| def test_sign_bytes(self): |
| to_sign = b'123' |
| signature = self.credentials.sign_bytes(to_sign) |
| assert crypt.verify_signature(to_sign, signature, PUBLIC_CERT_BYTES) |
| |
| def test_signer(self): |
| assert isinstance(self.credentials.signer, crypt.RSASigner) |
| |
| def test_signer_email(self): |
| assert (self.credentials.signer_email == |
| SERVICE_ACCOUNT_INFO['client_email']) |
| |
| def _verify_token(self, token): |
| payload = jwt.decode(token, PUBLIC_CERT_BYTES) |
| assert payload['iss'] == self.SERVICE_ACCOUNT_EMAIL |
| return payload |
| |
| def test_refresh(self): |
| self.credentials.refresh(None) |
| assert self.credentials.valid |
| assert not self.credentials.expired |
| |
| def test_expired(self): |
| assert not self.credentials.expired |
| |
| self.credentials.refresh(None) |
| assert not self.credentials.expired |
| |
| with mock.patch('google.auth._helpers.utcnow') as now: |
| one_day = datetime.timedelta(days=1) |
| now.return_value = self.credentials.expiry + one_day |
| assert self.credentials.expired |
| |
| def test_before_request(self): |
| headers = {} |
| |
| self.credentials.refresh(None) |
| self.credentials.before_request( |
| None, 'GET', 'http://example.com?a=1#3', headers) |
| |
| header_value = headers['authorization'] |
| _, token = header_value.split(' ') |
| |
| # Since the audience is set, it should use the existing token. |
| assert token.encode('utf-8') == self.credentials.token |
| |
| payload = self._verify_token(token) |
| assert payload['aud'] == self.AUDIENCE |
| |
| def test_before_request_refreshes(self): |
| assert not self.credentials.valid |
| self.credentials.before_request( |
| None, 'GET', 'http://example.com?a=1#3', {}) |
| assert self.credentials.valid |
| |
| |
| class TestOnDemandCredentials(object): |
| SERVICE_ACCOUNT_EMAIL = '[email protected]' |
| SUBJECT = 'subject' |
| ADDITIONAL_CLAIMS = {'meta': 'data'} |
| credentials = None |
| |
| @pytest.fixture(autouse=True) |
| def credentials_fixture(self, signer): |
| self.credentials = jwt.OnDemandCredentials( |
| signer, self.SERVICE_ACCOUNT_EMAIL, self.SERVICE_ACCOUNT_EMAIL, |
| max_cache_size=2) |
| |
| def test_from_service_account_info(self): |
| with open(SERVICE_ACCOUNT_JSON_FILE, 'r') as fh: |
| info = json.load(fh) |
| |
| credentials = jwt.OnDemandCredentials.from_service_account_info(info) |
| |
| assert credentials._signer.key_id == info['private_key_id'] |
| assert credentials._issuer == info['client_email'] |
| assert credentials._subject == info['client_email'] |
| |
| def test_from_service_account_info_args(self): |
| info = SERVICE_ACCOUNT_INFO.copy() |
| |
| credentials = jwt.OnDemandCredentials.from_service_account_info( |
| info, subject=self.SUBJECT, |
| additional_claims=self.ADDITIONAL_CLAIMS) |
| |
| assert credentials._signer.key_id == info['private_key_id'] |
| assert credentials._issuer == info['client_email'] |
| assert credentials._subject == self.SUBJECT |
| assert credentials._additional_claims == self.ADDITIONAL_CLAIMS |
| |
| def test_from_service_account_file(self): |
| info = SERVICE_ACCOUNT_INFO.copy() |
| |
| credentials = jwt.OnDemandCredentials.from_service_account_file( |
| SERVICE_ACCOUNT_JSON_FILE) |
| |
| assert credentials._signer.key_id == info['private_key_id'] |
| assert credentials._issuer == info['client_email'] |
| assert credentials._subject == info['client_email'] |
| |
| def test_from_service_account_file_args(self): |
| info = SERVICE_ACCOUNT_INFO.copy() |
| |
| credentials = jwt.OnDemandCredentials.from_service_account_file( |
| SERVICE_ACCOUNT_JSON_FILE, subject=self.SUBJECT, |
| additional_claims=self.ADDITIONAL_CLAIMS) |
| |
| assert credentials._signer.key_id == info['private_key_id'] |
| assert credentials._issuer == info['client_email'] |
| assert credentials._subject == self.SUBJECT |
| assert credentials._additional_claims == self.ADDITIONAL_CLAIMS |
| |
| def test_from_signing_credentials(self): |
| jwt_from_signing = self.credentials.from_signing_credentials( |
| self.credentials) |
| jwt_from_info = jwt.OnDemandCredentials.from_service_account_info( |
| SERVICE_ACCOUNT_INFO) |
| |
| assert isinstance(jwt_from_signing, jwt.OnDemandCredentials) |
| assert jwt_from_signing._signer.key_id == jwt_from_info._signer.key_id |
| assert jwt_from_signing._issuer == jwt_from_info._issuer |
| assert jwt_from_signing._subject == jwt_from_info._subject |
| |
| def test_default_state(self): |
| # Credentials are *always* valid. |
| assert self.credentials.valid |
| # Credentials *never* expire. |
| assert not self.credentials.expired |
| |
| def test_with_claims(self): |
| new_claims = {'meep': 'moop'} |
| new_credentials = self.credentials.with_claims( |
| additional_claims=new_claims) |
| |
| assert new_credentials._signer == self.credentials._signer |
| assert new_credentials._issuer == self.credentials._issuer |
| assert new_credentials._subject == self.credentials._subject |
| assert new_credentials._additional_claims == new_claims |
| |
| def test_sign_bytes(self): |
| to_sign = b'123' |
| signature = self.credentials.sign_bytes(to_sign) |
| assert crypt.verify_signature(to_sign, signature, PUBLIC_CERT_BYTES) |
| |
| def test_signer(self): |
| assert isinstance(self.credentials.signer, crypt.RSASigner) |
| |
| def test_signer_email(self): |
| assert (self.credentials.signer_email == |
| SERVICE_ACCOUNT_INFO['client_email']) |
| |
| def _verify_token(self, token): |
| payload = jwt.decode(token, PUBLIC_CERT_BYTES) |
| assert payload['iss'] == self.SERVICE_ACCOUNT_EMAIL |
| return payload |
| |
| def test_refresh(self): |
| with pytest.raises(exceptions.RefreshError): |
| self.credentials.refresh(None) |
| |
| def test_before_request(self): |
| headers = {} |
| |
| self.credentials.before_request( |
| None, 'GET', 'http://example.com?a=1#3', headers) |
| |
| _, token = headers['authorization'].split(' ') |
| payload = self._verify_token(token) |
| |
| assert payload['aud'] == 'http://example.com' |
| |
| # Making another request should re-use the same token. |
| self.credentials.before_request( |
| None, 'GET', 'http://example.com?b=2', headers) |
| |
| _, new_token = headers['authorization'].split(' ') |
| |
| assert new_token == token |
| |
| def test_expired_token(self): |
| self.credentials._cache['audience'] = ( |
| mock.sentinel.token, datetime.datetime.min) |
| |
| token = self.credentials._get_jwt_for_audience('audience') |
| |
| assert token != mock.sentinel.token |