2

I've been writing a API procedure to test posting to an http RestAPI with JwToken authentication. In this scenario it's for patient management system and I'm generating an appointment. The API business rule does not allow duplicate bookings for the same time.

I'm using a python 3.5.3 Virtual Environment (writing in pycharm IDE) and run my tests using the Pytest frame work. Also using PyJWT 1.5.2. , Requests 2.18.3, simplejson 3.11.1 and urllib3 1.22 is installed (I assume Requests is utilising urllib3). I'm using simplejson.dumps instead of the normal json.dumps because the virtual environment didn't have that library and I had trouble adding it. As far as I can tell simplejson has the same functionality for the dumps procedure.

Using the below code I find I can run requests.post call to deliver a Json data payload successfully and generate a post, but then it seems to subsequently perform a 2nd post which generates a 409 conflict error. I have access to the a log database on the corresponding api server and can see that it has in fact tried to post twice, but I can not work out why this occurs and I think there is something in the requests library that is being called twice. Possibly due to the Json I'm posting.

The output looks like this:

https://targerserver.url.com.au/API/Core/v2.1/appointment/
200
{'statusMessages': [], 'appointment': {'startDateTime': '2017-08-15T11:00:00 +10:00', 'appointmentReferenceNumber': '39960337', 'notes': '', 'clients': [{'clientId': 'abeff2be-ce6e-4324-9b57-e28ab7967b6c'}], 'status': 'Booked', 'locationId': 'd8d4fe7c-765a-46a3-a389-54ce298a27e9', 'notifyPractitioner': False, 'endDateTime': '2017-08-15T11:30:00 +10:00', 'subject': 'Jim Beam ', 'appointmentId': '08b37ce3-25e1-4e2a-9bb7-9ec2d716f83b', 'practitioner': {'practitionerId': 'a630f4ad-8b4b-4e06-8cee-7db56ba8b9bf'}}}
collected 1 item

test_PMSAPI_availability.py https://targerserver.url.com.au/API/Core/v2.1/appointment/
409

My Json requires an object (which is a dictionary) and also a list of keys for another field (that has one entry in it) and I'm wondering if the requests library is not handling this. This is a sample of what the json looks like

payload_str = {"startDateTime":"2017-08-15T11:00+10:00","endDateTime":"2017-08-15T11:30+10:00","practitioner": {"practitionerId":"a630f4ad-8b4b-4e06-8cee-7db56ba8b9bf"}, "locationId":"d8d4fe7c-765a-46a3-a389-54ce298a27e9","clients":[{"clientId":"abeff2be-ce6e-4324-9b57-e28ab7967b6c"}]}

I've similar code that has worked for Get calls on the same system, but posting the Json really seems to be problematic. We have other tools for making calls to the same API endpoints which do not seem to have this trouble. The logs would suggest that the JSON data I deliver is identical to that from another tool with the same data.

What I can see from the out put is that is receive a successful 200 code on the initial response, but then if query response.status_code it has become a 409 response. I also tried not doing anything with the response in case that was causing the request to requery and generate the conflict.

My code looks like this:

import jwt
import _datetime
import requests
import simplejson
from requests.packages.urllib3.exceptions import InsecureRequestWarning
from string import Template

def app_undertest_credentials(keyname):
    app_credentials = {'app_consumer_id': 'MyTestApp',
                       'app_consumer_secret': 'where my secret goes',
                       'app_access_token': 'where my access token goes',
                       'base_url': 'https://targerserver.url.com.au'
                       }

    return app_credentials.get(keyname)
def end_points_dict(keynameStr, versionStr):
    end_points = {'location': '/API/Core/$version/location/',
                  'practitioner': '/API/Core/$version/practitioner/',
                  'availabilityslot': '/API/Core/$version/AvailabilitySlot/',
                  'client': '/API/Core/$version/client/',
                  'healthfundproviderlist': '/API/Core/$version/healthfundproviderlist/',
                  'timezone': '/API/Core/$version/timezone/',
                  'clientgroup': '/API/Core/$version/clientgroup/',
                  'appointment': '/API/Core/$version/appointment/'
                  }
    lower_keynameStr = keynameStr.lower()
    url_extension_no_version = Template(end_points.get(lower_keynameStr))
    url_extension_with_version = url_extension_no_version.safe_substitute(version=versionStr)
    return url_extension_with_version

def test_api_appointment_post():
    # Set Client app credentials
    app_consumer_id = app_undertest_credentials('app_consumer_id')
    app_consumer_secret = app_undertest_credentials('app_consumer_secret')
    app_access_token = app_undertest_credentials('app_access_token')
    base_url = app_undertest_credentials('base_url')
    end_point_url_sfx_str = end_points_dict('Appointment', 'v2.1')
    httpmethod = 'POST'

    # Create dictionary for json post payload
    data_payload = {'startDateTime':'2017-08-15T11:00+10:00',
                'endDateTime':'2017-08-15T11:30+10:00',
                'practitioner': {'practitionerId':'a630f4ad-8b4b-4e06-8cee-7db56ba8b9bf'},
                'locationId': 'd8d4fe7c-765a-46a3-a389-54ce298a27e9',
                'clients': [{'clientId':'abeff2be-ce6e-4324-9b57-e28ab7967b6c'}]

    # Create claims dictionary payload for generation of JwToken
    claims = {
        'iss': 'http://myappsdomain.com.au',
        'aud': 'https://targetservers.domain.com.au',
        'nbf': _datetime.datetime.utcnow(),
        'exp': _datetime.datetime.utcnow() + _datetime.timedelta(seconds=60),
        'consumerId': app_consumer_id,
        'accessToken': app_access_token,
        'url': base_url + end_point_url_sfx_str,
        'httpMethod': http_method
    }

    #create jwtoken and then convert to string
    encoded_jwt_byte = jwt.encode(claim_payload, app_consumer_secret, algorithm='HS256')
    jwt_str = encoded_jwt_byte.decode()

    #Create authentication header
    headers = {'Authorization': 'JwToken' + ' ' + jwt_str, 'content-type': 'application/json'}

    uri = base_url + end_point_url_sfx_str
    response = requests.post(uri, headers=headers, json=datapayload)
    print(response.status)
    print(response.json())
    response.close()

I'm considering using wireshark to figure out what my call is actually sending but I suspect that will just tell me that the calls are being sent twice

Roochiedoor
  • 887
  • 12
  • 19
  • [Edit] your Question with which Python Module and Version you are useing, either `python_jwt` or `pyjwt`? – stovfl Aug 14 '17 at 15:34
  • I've added in the other module versions in use PyJWT 1.5.2. , Requests 2.18.3, simplejson 3.11.1 and urllib3 1.22 – Roochiedoor Aug 15 '17 at 03:26

2 Answers2

1

Comment: But I require a base64 encoded string for the sending the api requrest.

Regarding to the Source

segments.append(base64url_encode(signature))
    return base64.urlsafe_b64encode(input).replace(b'=', b'')
return b'.'.join(segments)

all is base64 and returned as bytes. So you should be fine using

jwt_str = str(encoded_jwt_byte)

Sorry, can't use PyJWT.
Tried with python_jwt and it works as expected.

Tested with Python:3.4.2 - requests:2.11.1

Do you realy need to encode(... and then decode()?

#create jwtoken and then convert to string
encoded_jwt_byte = jwt.encode(claim_payload, app_consumer_secret, algorithm='HS256')
jwt_str = encoded_jwt_byte.decode()

Will jwt_str not to be the same as claim_payload?

stovfl
  • 14,998
  • 7
  • 24
  • 51
  • For some reason with my system when I create the jwtoken using jwt.encode it creates byte code despite the documentation saying it should return a string. e.g. it looks like b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIU.... But I require a base64 encoded string for the sending the api requrest. By decoding again it creates a base64 encoded string which I can verify at https://jwt.io/ and seems to work for my get requests. If you have any suggestions why it is I receive a byte string instead of a base64 encoded string I'd be interested – Roochiedoor Aug 15 '17 at 07:17
  • Just realised I can use jwt_str = str(encoded_jwt_byte,'utf-8') instead of the decode which seems to be doing the same thing when it doesn't have the secret to decode with. – Roochiedoor Aug 15 '17 at 07:34
  • @Roochiedoor: Updated my Answer – stovfl Aug 15 '17 at 13:56
0

Okay found the cause of my duplicate post issue. It was my own stupid fault. Right down the bottom of my python file off the bottom of my screen I had called the test function and hadn't noticed this line (hence I missed posting it in my code above). It put the test function into a full repetitive loop. .... :( Such a stupid newbie mistake. Thanks to stovfl for the advice on handling the encoded JwToken.

In hindsight every post on stackoverflow I've found regarding duplicate posts to APIs has been because there was a loop in the user's code, I just couldn't find it.

Roochiedoor
  • 887
  • 12
  • 19