def test_get_access_token_no_claims(self, time, utcnow): utcnow.return_value = T1_DATE time.return_value = T1 token_info = self.jwt.get_access_token() payload = crypt.verify_signed_jwt_with_certs( token_info.access_token, {'key': datafile('public_cert.pem')}, audience=self.url) self.assertEqual(payload['iss'], self.service_account_email) self.assertEqual(payload['sub'], self.service_account_email) self.assertEqual(payload['iat'], T1) self.assertEqual(payload['exp'], T1_EXPIRY) self.assertEqual(token_info.expires_in, T1_EXPIRY - T1) # Verify that we vend the same token after 100 seconds utcnow.return_value = T2_DATE token_info = self.jwt.get_access_token() payload = crypt.verify_signed_jwt_with_certs( token_info.access_token, {'key': datafile('public_cert.pem')}, audience=self.url) self.assertEqual(payload['iat'], T1) self.assertEqual(payload['exp'], T1_EXPIRY) self.assertEqual(token_info.expires_in, T1_EXPIRY - T2) # Verify that we vend a new token after _MAX_TOKEN_LIFETIME_SECS utcnow.return_value = T3_DATE time.return_value = T3 token_info = self.jwt.get_access_token() payload = crypt.verify_signed_jwt_with_certs( token_info.access_token, {'key': datafile('public_cert.pem')}, audience=self.url) expires_in = token_info.expires_in self.assertEqual(payload['iat'], T3) self.assertEqual(payload['exp'], T3_EXPIRY) self.assertEqual(expires_in, T3_EXPIRY - T3)
def _check_jwt_failure(self, jwt, expected_error): public_key = datafile("publickey.pem") certs = {"foo": public_key} audience = "https://www.googleapis.com/auth/id?client_id=" "*****@*****.**" try: crypt.verify_signed_jwt_with_certs(jwt, certs, audience) self.fail() except crypt.AppIdentityError as e: self.assertTrue(expected_error in str(e))
def _check_jwt_failure(self, jwt, expected_error): public_key = datafile('publickey.pem') certs = {'foo': public_key} audience = ('https://www.googleapis.com/auth/id?client_id=' '*****@*****.**') try: crypt.verify_signed_jwt_with_certs(jwt, certs, audience) self.fail() except crypt.AppIdentityError as e: self.assertTrue(expected_error in str(e))
def test_jwt_no_segments(self): exception_caught = None try: crypt.verify_signed_jwt_with_certs(b'', None) except crypt.AppIdentityError as exc: exception_caught = exc self.assertNotEqual(exception_caught, None) self.assertTrue(str(exception_caught).startswith( 'Wrong number of segments in token'))
def _check_jwt_failure(self, jwt, expected_error): public_key = datafile('public_cert.pem') certs = {'foo': public_key} audience = ('https://www.googleapis.com/auth/id?client_id=' '*****@*****.**') with self.assertRaises(crypt.AppIdentityError) as exc_manager: crypt.verify_signed_jwt_with_certs(jwt, certs, audience) self.assertTrue(expected_error in str(exc_manager.exception))
def test_jwt_payload_bad_json(self): header = signature = b'' payload = base64.b64encode(b'{BADJSON') jwt = b'.'.join([header, payload, signature]) exception_caught = None try: crypt.verify_signed_jwt_with_certs(jwt, None) except crypt.AppIdentityError as exc: exception_caught = exc self.assertNotEqual(exception_caught, None) self.assertTrue(str(exception_caught).startswith('Can\'t parse token'))
def test_jwt_payload_bad_json(self): header = signature = b'' payload = base64.b64encode(b'{BADJSON') jwt = b'.'.join([header, payload, signature]) exception_caught = None try: crypt.verify_signed_jwt_with_certs(jwt, None) except crypt.AppIdentityError as exc: exception_caught = exc self.assertNotEqual(exception_caught, None) self.assertTrue(str(exception_caught).startswith( 'Can\'t parse token'))
def verify_id_token(id_token, audience, http=None, cert_uri=ID_TOKEN_VERIFICATON_CERTS): """Verifies a signed JWT id_token. Args: id_token: string, A Signed JWT. audience: string, The audience 'aud' that the token should be for. http: httplib2.Http, instance to use to make the HTTP request. Callers should supply an instance that has caching enabled. cert_uri: string, URI of the certificates in JSON format to verify the JWT against. Returns: The deserialized JSON in the JWT. Raises: oauth2client.crypt.AppIdentityError if the JWT fails to verify. """ if http is None: http = _cached_http resp, content = http.request(cert_uri) if resp.status == 200: certs = simplejson.loads(content) return verify_signed_jwt_with_certs(id_token, certs, audience) else: raise VerifyJwtTokenError('Status code: %d' % resp.status)
def test_authorize_success(self, time, utcnow): utcnow.return_value = T1_DATE time.return_value = T1 http = http_mock.HttpMockSequence([ ({'status': http_client.OK}, b''), ({'status': http_client.OK}, b''), ]) self.jwt.authorize(http) transport.request(http, self.url) # Ensure we use the cached token utcnow.return_value = T2_DATE transport.request(http, self.url) # Verify mocks. certs = {'key': datafile('public_cert.pem')} self.assertEqual(len(http.requests), 2) for info in http.requests: self.assertEqual(info['method'], 'GET') self.assertEqual(info['uri'], self.url) self.assertIsNone(info['body']) self.assertEqual(len(info['headers']), 1) bearer, token = info['headers'][b'Authorization'].split() self.assertEqual(bearer, b'Bearer') payload = crypt.verify_signed_jwt_with_certs( token, certs, audience=self.url) self.assertEqual(len(payload), 5) self.assertEqual(payload['iss'], self.service_account_email) self.assertEqual(payload['sub'], self.service_account_email) self.assertEqual(payload['iat'], T1) self.assertEqual(payload['exp'], T1_EXPIRY) self.assertEqual(payload['aud'], self.url)
def test_verify_id_token(self): jwt = self._create_signed_jwt() public_key = datafile('publickey.pem') certs = {'foo': public_key } audience = '*****@*****.**' contents = crypt.verify_signed_jwt_with_certs(jwt, certs, audience) self.assertEqual('billy bob', contents['user']) self.assertEqual('data', contents['metadata']['meta'])
def test_verify_id_token(self): jwt = self._create_signed_jwt() public_key = datafile("publickey.pem") certs = {"foo": public_key} audience = "*****@*****.**" contents = crypt.verify_signed_jwt_with_certs(jwt, certs, audience) self.assertEqual("billy bob", contents["user"]) self.assertEqual("data", contents["metadata"]["meta"])
def _check_jwt_failure(self, jwt, expected_error): try: public_key = datafile('publickey.pem') certs = {'foo': public_key} audience = 'https://www.googleapis.com/auth/id?client_id=' + \ '*****@*****.**' contents = crypt.verify_signed_jwt_with_certs(jwt, certs, audience) self.fail('Should have thrown for %s' % jwt) except: e = sys.exc_info()[1] msg = e.args[0] self.assertTrue(expected_error in msg)
def test_authorize_401(self, utcnow): utcnow.return_value = T1_DATE http = http_mock.HttpMockSequence([ ({ 'status': http_client.OK }, b''), ({ 'status': http_client.UNAUTHORIZED }, b''), ({ 'status': http_client.OK }, b''), ]) self.jwt.authorize(http) transport.request(http, self.url) token_1 = self.jwt.access_token utcnow.return_value = T2_DATE response, _ = transport.request(http, self.url) self.assertEquals(response.status, http_client.OK) token_2 = self.jwt.access_token # Check the 401 forced a new token self.assertNotEqual(token_1, token_2) # Verify mocks. certs = {'key': datafile('public_cert.pem')} self.assertEqual(len(http.requests), 3) issued_at_vals = (T1, T1, T2) exp_vals = (T1_EXPIRY, T1_EXPIRY, T2_EXPIRY) for info, issued_at, exp_val in zip(http.requests, issued_at_vals, exp_vals): self.assertEqual(info['uri'], self.url) self.assertEqual(info['method'], 'GET') self.assertIsNone(info['body']) self.assertEqual(len(info['headers']), 1) bearer, token = info['headers'][b'Authorization'].split() self.assertEqual(bearer, b'Bearer') # To parse the token, skip the time check, since this # test intentionally has stale tokens. with mock.patch('oauth2client.crypt._verify_time_range', return_value=True): payload = crypt.verify_signed_jwt_with_certs(token, certs, audience=self.url) self.assertEqual(len(payload), 5) self.assertEqual(payload['iss'], self.service_account_email) self.assertEqual(payload['sub'], self.service_account_email) self.assertEqual(payload['iat'], issued_at) self.assertEqual(payload['exp'], exp_val) self.assertEqual(payload['aud'], self.url)
def mock_request(uri, method='GET', body=None, headers=None, redirections=0, connection_type=None): self.assertEqual(uri, self.url) bearer, token = headers[b'Authorization'].split() payload = crypt.verify_signed_jwt_with_certs( token, {'key': datafile('public_cert.pem')}, audience=self.url) self.assertEqual(payload['iss'], self.service_account_email) self.assertEqual(payload['sub'], self.service_account_email) self.assertEqual(payload['iat'], T1) self.assertEqual(payload['exp'], T1_EXPIRY) self.assertEqual(uri, self.url) self.assertEqual(bearer, b'Bearer') return (httplib2.Response({'status': '200'}), b'')
def verify_google_id_token(token): """Verify the id token against the Google public keys. Note that this function does *not* validate the hosted domain (hd) claim. It *does* verify the issuer (iss) claim. If verification fails, an oauth2client.crypt.AppIdentityError is raised. """ certs = current_app.config.get('GOOGLE_OAUTH2_CERTS', _get_default_certs()) client_ids = current_app.config.get('GOOGLE_OAUTH2_ALLOWED_CLIENT_IDS', []) idinfo = crypt.verify_signed_jwt_with_certs(token, certs, None) if idinfo.get('aud') not in client_ids: raise crypt.AppIdentityError('invalid aud: %s' % idinfo.get('aud')) if idinfo.get('iss') not in _GOOGLE_ISSUERS: raise crypt.AppIdentityError('invalid issuer: %s' % idinfo.get('iss')) return idinfo
def get_token(): """ Creates an authentication token for valid service credentials. Returns: A JSON string containing a bearer token. """ config = current_app.config try: jwt = request.form['assertion'] except KeyError: return error('JWT assertion not defined.', HTTP_BAD_REQUEST) valid_users = config['USERS'] public_keys = {} for user in valid_users: with open(valid_users[user]['certificate']) as certificate: public_keys[user] = certificate.read() try: payload = verify_signed_jwt_with_certs(jwt, public_keys, audience=request.url) except AppIdentityError: return error('Unable to verify assertion.', HTTP_UNAUTHORIZED) try: user = payload['iss'] except KeyError: return error('User not defined in JWT assertion.', HTTP_BAD_REQUEST) if user not in valid_users: return error('{} not configured.'.format(user), HTTP_UNAUTHORIZED) token = ''.join( random.choice(string.ascii_letters + string.digits + '._-') for _ in range(config['ACCESS_TOKEN_LENGTH'])) current_app.logger.debug('new token: {}'.format(token)) expiration = datetime.timedelta(seconds=config['TOKEN_EXPIRATION']) set_token(token, user, expiration=datetime.datetime.now() + expiration) response = { 'access_token': token, 'token_type': 'Bearer', 'expires_in': config['TOKEN_EXPIRATION'] } return Response(json.dumps(response), mimetype='application/json')
def VerifyGitkitToken(self, jwt): """Verifies a Gitkit token string. Args: jwt: string, the token to be checked Returns: GitkitUser, if the token is valid. None otherwise. """ certs = self.rpc_helper.GetPublicCert() crypt.MAX_TOKEN_LIFETIME_SECS = 30 * 86400 # 30 days try: parsed = crypt.verify_signed_jwt_with_certs(jwt, certs, self.client_id) return GitkitUser.FromToken(parsed) except crypt.AppIdentityError: return None
def VerifyGitkitToken(self, jwt): """Verifies a Gitkit token string. Args: jwt: string, the token to be checked Returns: GitkitUser, if the token is valid. None otherwise. """ certs = self.rpc_helper.GetPublicCert() crypt.MAX_TOKEN_LIFETIME_SECS = 30 * 86400 # 30 days try: parsed = crypt.verify_signed_jwt_with_certs( jwt, certs, self.client_id) return GitkitUser.FromToken(parsed) except crypt.AppIdentityError: return None
def test_get_access_token_additional_claims(self, time, utcnow): utcnow.return_value = T1_DATE time.return_value = T1 token_info = self.jwt.get_access_token(additional_claims={ 'aud': 'https://test2.url.com', 'sub': '*****@*****.**' }) payload = crypt.verify_signed_jwt_with_certs( token_info.access_token, {'key': datafile('public_cert.pem')}, audience='https://test2.url.com') expires_in = token_info.expires_in self.assertEqual(payload['iss'], self.service_account_email) self.assertEqual(payload['sub'], '*****@*****.**') self.assertEqual(payload['iat'], T1) self.assertEqual(payload['exp'], T1_EXPIRY) self.assertEqual(expires_in, T1_EXPIRY - T1)
def test_get_access_token_additional_claims(self, time, utcnow): utcnow.return_value = T1_DATE time.return_value = T1 token_info = self.jwt.get_access_token(additional_claims= {'aud': 'https://test2.url.com', 'sub': '*****@*****.**' }) payload = crypt.verify_signed_jwt_with_certs( token_info.access_token, {'key' : datafile('public_cert.pem')}, audience='https://test2.url.com') expires_in = token_info.expires_in self.assertEqual(payload['iss'], self.service_account_email) self.assertEqual(payload['sub'], '*****@*****.**') self.assertEqual(payload['iat'], T1) self.assertEqual(payload['exp'], T1_EXPIRY) self.assertEqual(expires_in, T1_EXPIRY - T1)
def test_authorize_401(self, utcnow): utcnow.return_value = T1_DATE http = http_mock.HttpMockSequence([ ({'status': http_client.OK}, b''), ({'status': http_client.UNAUTHORIZED}, b''), ({'status': http_client.OK}, b''), ]) self.jwt.authorize(http) transport.request(http, self.url) token_1 = self.jwt.access_token utcnow.return_value = T2_DATE response, _ = transport.request(http, self.url) self.assertEquals(response.status, http_client.OK) token_2 = self.jwt.access_token # Check the 401 forced a new token self.assertNotEqual(token_1, token_2) # Verify mocks. certs = {'key': datafile('public_cert.pem')} self.assertEqual(len(http.requests), 3) issued_at_vals = (T1, T1, T2) exp_vals = (T1_EXPIRY, T1_EXPIRY, T2_EXPIRY) for info, issued_at, exp_val in zip(http.requests, issued_at_vals, exp_vals): self.assertEqual(info['uri'], self.url) self.assertEqual(info['method'], 'GET') self.assertIsNone(info['body']) self.assertEqual(len(info['headers']), 1) bearer, token = info['headers'][b'Authorization'].split() self.assertEqual(bearer, b'Bearer') # To parse the token, skip the time check, since this # test intentionally has stale tokens. with mock.patch('oauth2client.crypt._verify_time_range', return_value=True): payload = crypt.verify_signed_jwt_with_certs( token, certs, audience=self.url) self.assertEqual(len(payload), 5) self.assertEqual(payload['iss'], self.service_account_email) self.assertEqual(payload['sub'], self.service_account_email) self.assertEqual(payload['iat'], issued_at) self.assertEqual(payload['exp'], exp_val) self.assertEqual(payload['aud'], self.url)
def verify_id_token(id_token, cert_uri, audience=None, kid=None, http=None): """Verifies the provided ID token. Checks for token integrity by verifying its signature against a set of public key certificates. Certificates are downloaded from cert_uri, and cached according to the HTTP cache control requirements. If provided, the audience and kid fields of the ID token are also validated. Args: id_token: JWT ID token to be validated. cert_uri: A URI string pointing to public key certificates. audience: Audience string that should be present in the token. kid: JWT key ID header to locate the public key certificate. http: An httplib2 HTTP client instance. Returns: dict: A dictionary of claims extracted from the ID token. Raises: ValueError: Certificate URI is None or empty. AppIdentityError: Token integrity check failed. VerifyJwtTokenError: Failed to load public keys or invalid kid header. """ if not cert_uri: raise ValueError('Certificate URI is required') if not http: http = _cached_http resp, content = http.request(cert_uri) if resp.status != 200: raise client.VerifyJwtTokenError( ('Failed to load public key certificates from URL "{0}". Received ' 'HTTP status code {1}.').format(cert_uri, resp.status)) str_content = content.decode('utf-8') if isinstance( content, six.binary_type) else content certs = json.loads(str_content) if kid and kid not in certs: raise client.VerifyJwtTokenError( 'Firebase ID token has "kid" claim which does' ' not correspond to a known public key. Most' ' likely the ID token is expired, so get a' ' fresh token from your client app and try again.') return crypt.verify_signed_jwt_with_certs(id_token, certs, audience)
def test_authorize_no_aud(self, time, utcnow): utcnow.return_value = T1_DATE time.return_value = T1 jwt = service_account._JWTAccessCredentials( self.service_account_email, self.signer, private_key_id=self.private_key_id, client_id=self.client_id) http = http_mock.HttpMockSequence([ ({ 'status': http_client.OK }, b''), ]) jwt.authorize(http) transport.request(http, self.url) # Ensure we do not cache the token self.assertIsNone(jwt.access_token) # Verify mocks. self.assertEqual(len(http.requests), 1) info = http.requests[0] self.assertEqual(info['method'], 'GET') self.assertEqual(info['uri'], self.url) self.assertIsNone(info['body']) self.assertEqual(len(info['headers']), 1) bearer, token = info['headers'][b'Authorization'].split() self.assertEqual(bearer, b'Bearer') certs = {'key': datafile('public_cert.pem')} payload = crypt.verify_signed_jwt_with_certs(token, certs, audience=self.url) self.assertEqual(len(payload), 5) self.assertEqual(payload['iss'], self.service_account_email) self.assertEqual(payload['sub'], self.service_account_email) self.assertEqual(payload['iat'], T1) self.assertEqual(payload['exp'], T1_EXPIRY) self.assertEqual(payload['aud'], self.url)
def VerifyGitkitToken(self, jwt): """Verifies a Gitkit token string. Args: jwt: string, the token to be checked Returns: GitkitUser, if the token is valid. None otherwise. """ certs = self.rpc_helper.GetPublicCert() crypt.MAX_TOKEN_LIFETIME_SECS = 30 * 86400 # 30 days parsed = None for aud in filter(lambda x: x is not None, [self.project_id, self.client_id]): try: parsed = crypt.verify_signed_jwt_with_certs(jwt, certs, aud) except crypt.AppIdentityError as e: if "Wrong recipient" not in e.message: return None if parsed: return GitkitUser.FromToken(parsed) return None # Gitkit token audience doesn't match projectId or clientId in server configuration
def test_authorize_success(self, time, utcnow): utcnow.return_value = T1_DATE time.return_value = T1 http = http_mock.HttpMockSequence([ ({ 'status': http_client.OK }, b''), ({ 'status': http_client.OK }, b''), ]) self.jwt.authorize(http) transport.request(http, self.url) # Ensure we use the cached token utcnow.return_value = T2_DATE transport.request(http, self.url) # Verify mocks. certs = {'key': datafile('public_cert.pem')} self.assertEqual(len(http.requests), 2) for info in http.requests: self.assertEqual(info['method'], 'GET') self.assertEqual(info['uri'], self.url) self.assertIsNone(info['body']) self.assertEqual(len(info['headers']), 1) bearer, token = info['headers'][b'Authorization'].split() self.assertEqual(bearer, b'Bearer') payload = crypt.verify_signed_jwt_with_certs(token, certs, audience=self.url) self.assertEqual(len(payload), 5) self.assertEqual(payload['iss'], self.service_account_email) self.assertEqual(payload['sub'], self.service_account_email) self.assertEqual(payload['iat'], T1) self.assertEqual(payload['exp'], T1_EXPIRY) self.assertEqual(payload['aud'], self.url)
def test_authorize_no_aud(self, time, utcnow): utcnow.return_value = T1_DATE time.return_value = T1 jwt = service_account._JWTAccessCredentials( self.service_account_email, self.signer, private_key_id=self.private_key_id, client_id=self.client_id) http = http_mock.HttpMockSequence([ ({'status': http_client.OK}, b''), ]) jwt.authorize(http) transport.request(http, self.url) # Ensure we do not cache the token self.assertIsNone(jwt.access_token) # Verify mocks. self.assertEqual(len(http.requests), 1) info = http.requests[0] self.assertEqual(info['method'], 'GET') self.assertEqual(info['uri'], self.url) self.assertIsNone(info['body']) self.assertEqual(len(info['headers']), 1) bearer, token = info['headers'][b'Authorization'].split() self.assertEqual(bearer, b'Bearer') certs = {'key': datafile('public_cert.pem')} payload = crypt.verify_signed_jwt_with_certs( token, certs, audience=self.url) self.assertEqual(len(payload), 5) self.assertEqual(payload['iss'], self.service_account_email) self.assertEqual(payload['sub'], self.service_account_email) self.assertEqual(payload['iat'], T1) self.assertEqual(payload['exp'], T1_EXPIRY) self.assertEqual(payload['aud'], self.url)
def test_success(self, verify_sig, verify_time, check_aud): certs = mock.MagicMock() cert_values = object() certs.values = mock.MagicMock(name='values', return_value=cert_values) audience = object() header = b'header' signature_bytes = b'signature' signature = base64.b64encode(signature_bytes) payload_dict = {'a': 'b'} payload = base64.b64encode(b'{"a": "b"}') jwt = b'.'.join([header, payload, signature]) result = crypt.verify_signed_jwt_with_certs( jwt, certs, audience=audience) self.assertEqual(result, payload_dict) message_to_sign = header + b'.' + payload verify_sig.assert_called_once_with( message_to_sign, signature_bytes, cert_values) verify_time.assert_called_once_with(payload_dict) check_aud.assert_called_once_with(payload_dict, audience) certs.values.assert_called_once_with()