Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

possible to add params to header ? #125

Closed
spaceexperiment opened this issue May 22, 2014 · 22 comments
Closed

possible to add params to header ? #125

spaceexperiment opened this issue May 22, 2014 · 22 comments

Comments

@spaceexperiment
Copy link

Like i have to add body_hash_tag in the authorization header, but i can't seem to find a legit way to do it.
is it no possible with this library ?

@ib-lundgren
Copy link
Member

There is currently no easy way to add extra parameters to the authorization headers in OAuth 1 or 2.

What is your use case? Do you use OAuth 1 or 2? For 2 I don't think Bearer tokens usually have parameters, although there is nothing stopping you from embedding it into the token itself (e.g. JWT).

@spaceexperiment
Copy link
Author

@ib-lundgren
Yes it's oauth1, for they require the oauth_body_hash, I have searched even different libraries even if they had added params, they didn't support rsa-sha1, could you advice on anyway to do it through this library ? or any other for that matter ?

@ib-lundgren
Copy link
Member

Adding custom parameters makes more sense in OAuth1 and we could probably do that. I am not familiar with oauth_body_hash but guessing from the name they add a custom integrity protecting param which is a hash over the request body.

Is the provider public? Would be nice to look into their docs.

@spaceexperiment
Copy link
Author

@ib-lundgren
yes it's base64 encoded sha1 hash for the body, the integration is for masterpass https://www.mastercard.com/mc_us/wallet/guides/merchant/sp/merchantbymerchant/index.html#me_section_4 on the shopping cart section.
Unfortunately they have no SDK or any code for python.

@ib-lundgren
Copy link
Member

Cheers, will look into their docs to see if there are more things missing but the oauth_body_hash should be no problem to add. It is a reasonable addition to the spec but will have a think about whether to include it directly in OAuthlib or as a provider specific work around with a custom client here.

I have a ton of issues/PRs to get through since exams sucked my time the last two months but stay tuned for this feature in the coming week. I don't have any master pass account so will likely need your help testing it out :)

@spaceexperiment
Copy link
Author

Thanks @ib-lundgren
Would love to help out, in the mean time is there a quick fix for this?
Thanks

@ib-lundgren
Copy link
Member

Not sure I'd go as far as calling it a fix but if you want to start experimenting then

from __future__ import unicode_literals

from hashlib import sha1

from requests_oauthlib import OAuth1Session
from oauthlib.oauth1 import Client
from oauthlib.common import to_unicode

class BodyHashClient(Client):
    """Adds a custom body integrity protecting oauth parameter oauth_body_hash
    that is required by the provider MasterPass.
    """

    def get_oauth_params(self, request):
        params = super(BodyHashClient, self).get_oauth_params(request)
        # TODO: check if they want hash for empty body.
        digest = to_unicode(sha1(request.body or '').hexdigest())
        params.append(('oauth_body_hash', digest))
        return params


oauth = OAuth1Session(...., client_class=BodyHashClient)
  • Let me know what happens =)

This will add oauth_body_hash (SHA1 signature of body text) but I've not looked into what else might be required by masterpass yet.

@spaceexperiment
Copy link
Author

Thanks for taking you time with this @ib-lundgren
when i am calling post on it with the data i am getting this error returned
The oauth_body_hash parameter does not match body contents and is invalid.

I then tried writing this function
'''
def get_body_hash(xml):
sha1 = hashlib.sha1()
sha1.update(xml)
oauth_body_hash = sha1.digest()
return base64.b64encode(oauth_body_hash)
'''
Then i got invalid or incomplete signature

@ib-lundgren
Copy link
Member

Ah the hash is base64 encoded and not hex , my bad.

Are you using hat code to replace digest in the get_oauth_params method I suggested? Because it seems like their validation order is first body hash then signature, with your hash being correct but the code not being used in the oauth base string during signature creation (which the code i gave does) and thus giving an incorrect signature.

@spaceexperiment
Copy link
Author

@ib-lundgren yes i have tried to replace the code on the digest var in you code,
is the signature cerated on a call to the request ?

@spaceexperiment
Copy link
Author

@ib-lundgren the request.body in get_auth_params seems to be empty when calling
oauth.post(url=request_token_url, data=xml)

@ib-lundgren
Copy link
Member

Hrm, will look into the body being empty. Sounds like the body is not propagated properly, maybe I am using the wrong attribute on the requests.request object.

ib-lundgren added a commit that referenced this issue Jun 4, 2014
@ib-lundgren
Copy link
Member

Forgot that requests-oauthlib strips body before passing the request onwards to oauthlib unless the content type is urlencoded (best approach for most occasions). It is trouble for us here since we want to hash over the body, which is done from within oauthlib and get_oauth_params. I've updated master now to include a new force_include_body parameter which our custom client can use.

Would you mind checking out newest master and trying

# -*- coding: UTF-8 -*-
from __future__ import unicode_literals

from hashlib import sha1
from base64 import b64encode

from requests_oauthlib import OAuth1Session
from oauthlib.oauth1 import Client
from oauthlib.common import to_unicode

class BodyHashClient(Client):

    def get_oauth_params(self, request):
        params = super(BodyHashClient, self).get_oauth_params(request)
        digest = b64encode(sha1(request.body.encode('UTF-8')).digest())
        params.append(('oauth_body_hash', to_unicode(digest)))
        return params


oauth = OAuth1Session(..., client_class=BodyHashClient, force_include_body=True)

@spaceexperiment
Copy link
Author

Yes the body is now included in the request, and it seem to pass the auth_body_hash just fine.
But now i am getting this error invalid signature, and i cant seem to figure out why

<Errors>\n<Error>\n  <Source>OAuth.Signature</Source>\n  <ReasonCode>AUTHENTICATION_FAILED</ReasonCode>\n  <Description>The request contains an incomplete or invalid oauth_signature.</Description>\n  <Recoverable>false</Recoverable>\n  <Details>\n    <Detail>\n      <Name>Allowed OAuth Signature Base String 1</Name>\n      <Value><![CDATA[\nPOST& 
...............   striped rest of body

@ib-lundgren
Copy link
Member

Hrm, tricky since it can be a number of things and for security reasons they can't really help out with a more detailed error message.

After skimming their docs the only thing that pops out is that the token should not be sent in the request. Or at least not in "postback" but possibly in "shopping cart", not sure what you are calling. Could you check whether that is the case?

...
r = oauth.post(....)
print r.request.headers

If you have time, enable oauthlib logging, scrub and paste the output here

import logging
import sys
log = logging.getLogger('oauthlib')
log.addHandler(logging.StreamHandler(sys.stdout))
log.setLevel(logging.DEBUG)

@spaceexperiment
Copy link
Author

Here is the relevant code with with the request headers and the log info.

def get_credentials():
    oauth = OAuth1Session(
        consumer_key,
        signature_method='RSA-SHA1',
        signature_type='auth_header',
        rsa_key=key,
        callback_uri=callback_uri
    )
    return oauth.fetch_request_token(request_token_url,
                                     realm=['eWallet'])

def get_session(credentials):
    credentials = get_credentials()
    return OAuth1Session(
        consumer_key,
        signature_method='RSA-SHA1',
        signature_type='auth_header',
        rsa_key=key,
        callback_uri=callback_uri,
        resource_owner_key=credentials['oauth_token'],
        client_class=BodyHashClient,
        force_include_body=True
    )


credentials = get_credentials()

this returns the credentials fine
log info

Collected params: [(u'oauth_nonce', u'87326368174508108781401969719'), (u'oauth_timestamp', u'1401969719'), (u'oauth_consumer_key', u'QAJO-UfhDtTLsUkuOJXyks6gEc4v0ueowUscPisU5e403949!6337436d383951652b5069627a47417354566c6a76773d3d'), (u'oauth_signature_method', u'RSA-SHA1'), (u'oauth_version', u'1.0'), (u'oauth_callback', u'https://localhost:8080/merchant/callback')]
Normalized params: oauth_callback=https%3A%2F%2Flocalhost%3A8080%2Fmerchant%2Fcallback&oauth_consumer_key=QAJO-UfhDtTLsUkuOJXyks6gEc4v0ueowUscPisU5e403949%216337436d383951652b5069627a47417354566c6a76773d3d&oauth_nonce=87326368174508108781401969719&oauth_signature_method=RSA-SHA1&oauth_timestamp=1401969719&oauth_version=1.0
Normalized URI: https://sandbox.api.mastercard.com/oauth/consumer/v1/request_token
Base signing string: POST&https%3A%2F%2Fsandbox.api.mastercard.com%2Foauth%2Fconsumer%2Fv1%2Frequest_token&oauth_callback%3Dhttps%253A%252F%252Flocalhost%253A8080%252Fmerchant%252Fcallback%26oauth_consumer_key%3DQAJO-UfhDtTLsUkuOJXyks6gEc4v0ueowUscPisU5e403949%25216337436d383951652b5069627a47417354566c6a76773d3d%26oauth_nonce%3D87326368174508108781401969719%26oauth_signature_method%3DRSA-SHA1%26oauth_timestamp%3D1401969719%26oauth_version%3D1.0
Signature: fRakM4zvd5ZL9B0MY5g0POMD/dkTt0XRWG1s8mzLmtGrIfBPlYY2I1aWUPsVxTg9eDynpGqfKQuhIXjOT/NI6NDFz1xgsyN1mK10imOA5tLhQ/ZkI4hdkxC/OQf0iuzqvqgHfNlTgK47vlFf3yksmYiGtprFuA8VZIUXbxXwf67CyQtoJm5TTsNll5ZLNxoYDD4LN8RgpFhEDv648MZfao/zAqjt1EZ4o/6FrXUHmm6m6xfdJel25DUGHYLfvFab/vsSmM1/bRr0OEEW53cPwJy4E770RMQKzniF1FnKkqzYO6VCcCcAikarSsnqYxyxfCIHDpQgC9PvYrU12Toldg==
Encoding URI, headers and body to utf-8.

# xml body
root = ET.fromstring(shopping_cart_xml)
root.find('OAuthToken').text = credentials['oauth_token']
xml = ET.tostring(root, encoding="utf8", method="xml")

oauth = get_session(credentials)

resp = oauth.post(url=shopping_cart_url, data=xml)

print resp.request.headers

{
'Accept': '*/*',
'Content-Length': u'620',
'Accept-Encoding': 'gzip, deflate, compress',
'Authorization': 'OAuth oauth_nonce="170519491205601708881401969266", oauth_timestamp="1401969266", oauth_version="1.0", oauth_signature_method="RSA-SHA1", oauth_consumer_key="QAJO-UfhDtTLsUkuOJXyks6gEc4v0ueowUscPisU5e403949%216337436d383951652b5069627a47417354566c6a76773d3d", oauth_token="6fb931022e7c85c1b50b71305f46b503", oauth_callback="https%3A%2F%2Flocalhost%3A8080%2Fmerchant%2Fcallback", oauth_body_hash="koQbcXP2tl7Qky41npp27kI4Bf8%3D", oauth_signature="jDu4a6EBOl5CTXf9cl3axIjjz%2BlF%2FKcap6CBgnZ3dtDT3RhEzwqzXVXUTX4kE8xRVVMHwMVndansZ3KSe%2FDBty0iIrK0HWaHdiUCZqxjm3sj4uM7jspJS20RSTRp%2BZmuUwSWLlRrmkZROXfO1e3psHvZIKypNt72YGgyQcC%2B5QwWsRWkwwr4uzVumiQVCy5GqfL70s4RWrk510Zfc8H5SJIETzYbf%2BQU4CQq2HOXJDD4QQBd830Q%2B%2FaX%2BymE2DkxffiDBob3uA34tXAw0gey3g%2FKkaCrpQHTLNwulQAhS095iWQu8kuJlWSvTvQ6khqLpwmcsdVkFi9Swqek8kr9zA%3D%3D"', 'User-Agent': 'python-requests/2.0.1 CPython/2.7.5+ Linux/3.11.0-22-generic'
}

print resp.text

<Errors>\n<Error>\n  <Source>OAuth.Signature</Source>\n  <ReasonCode>AUTHENTICATION_FAILED</ReasonCode>\n  <Description>The request contains an incomplete or invalid oauth_signature.</Description>\n  <Recoverable>false</Recoverable>\n  <Details>\n    <Detail>\n      <Name>Allowed OAuth Signature Base String 1</Name>\n      <Value><![CDATA[\nPOST&https%3A%2F%2Fsandbox.api.mastercard.com%2Fonline%2Fv1%2Fshopping-cart&oauth_body_hash%3DkoQbcXP2tl7Qky41npp27kI4Bf8%3D%26oauth_callback%3Dhttps%3A%2F%2Flocalhost%3A8080%2Fmerchant%2Fcallback%26oauth_consumer_key%3DQAJO-UfhDtTLsUkuOJXyks6gEc4v0ueowUscPisU5e403949%216337436d383951652b5069627a47417354566c6a76773d3d%26oauth_nonce%3D68485092433305047151401969850%26oauth_signature_method%3DRSA-SHA1%26oauth_timestamp%3D1401969850%26oauth_token%3D6fb931022e7c85c1b50b71305f46b503%26oauth_version%3D1.0\n]]></Value>\n    </Detail>\n    <Detail>\n      <Name>Allowed OAuth Signature Base String 2</Name>\n      <Value><![CDATA[\nPOST&https%3A%2F%2Fsandbox.api.mastercard.com%2Fonline%2Fv1%2Fshopping-cart&oauth_body_hash%3DkoQbcXP2tl7Qky41npp27kI4Bf8%253D%26oauth_callback%3Dhttps%253A%252F%252Flocalhost%253A8080%252Fmerchant%252Fcallback%26oauth_consumer_key%3DQAJO-UfhDtTLsUkuOJXyks6gEc4v0ueowUscPisU5e403949%25216337436d383951652b5069627a47417354566c6a76773d3d%26oauth_nonce%3D68485092433305047151401969850%26oauth_signature_method%3DRSA-SHA1%26oauth_timestamp%3D1401969850%26oauth_token%3D6fb931022e7c85c1b50b71305f46b503%26oauth_version%3D1.0\n]]></Value>\n    </Detail>\n  </Details>\n</Error>\n</Errors>\n

log info

Collected params: [(u'oauth_body_hash', u'koQbcXP2tl7Qky41npp27kI4Bf8='), (u'oauth_nonce', u'68485092433305047151401969850'), (u'oauth_timestamp', u'1401969850'), (u'oauth_consumer_key', u'QAJO-UfhDtTLsUkuOJXyks6gEc4v0ueowUscPisU5e403949!6337436d383951652b5069627a47417354566c6a76773d3d'), (u'oauth_signature_method', u'RSA-SHA1'), (u'oauth_version', u'1.0'), (u'oauth_token', u'6fb931022e7c85c1b50b71305f46b503'), (u'oauth_callback', u'https://localhost:8080/merchant/callback')]
Normalized params: oauth_body_hash=koQbcXP2tl7Qky41npp27kI4Bf8%3D&oauth_callback=https%3A%2F%2Flocalhost%3A8080%2Fmerchant%2Fcallback&oauth_consumer_key=QAJO-UfhDtTLsUkuOJXyks6gEc4v0ueowUscPisU5e403949%216337436d383951652b5069627a47417354566c6a76773d3d&oauth_nonce=68485092433305047151401969850&oauth_signature_method=RSA-SHA1&oauth_timestamp=1401969850&oauth_token=6fb931022e7c85c1b50b71305f46b503&oauth_version=1.0
Normalized URI: https://sandbox.api.mastercard.com//online/v1/shopping-cart
Base signing string: POST&https%3A%2F%2Fsandbox.api.mastercard.com%2F%2Fonline%2Fv1%2Fshopping-cart&oauth_body_hash%3DkoQbcXP2tl7Qky41npp27kI4Bf8%253D%26oauth_callback%3Dhttps%253A%252F%252Flocalhost%253A8080%252Fmerchant%252Fcallback%26oauth_consumer_key%3DQAJO-UfhDtTLsUkuOJXyks6gEc4v0ueowUscPisU5e403949%25216337436d383951652b5069627a47417354566c6a76773d3d%26oauth_nonce%3D68485092433305047151401969850%26oauth_signature_method%3DRSA-SHA1%26oauth_timestamp%3D1401969850%26oauth_token%3D6fb931022e7c85c1b50b71305f46b503%26oauth_version%3D1.0
Signature: L3SenJ0QJP+ARVqPnZ8+DfL2XNLJ5d9wyudBtTuKQua8kVJ9VxONt97tV2TA7HyRPlis252OEhE6kq9Y8unt50MXtuBAk3jcuVnZkkJXHaz7mCJszkhpV+9P+wr9vMZqwNCkkR+ituqZyTWgs6ziCqrgYO2EKNufD/G5jdyQOnuJOdty6cJRWjQbOXJ8kKaxTpJ0NaSiYYsGRSzT/qNs1cBIvWZFcelN++cE/7vPAw+Ym3G7H161QS48hx8iZalS45/wqY7DVFtOndVKpfMYW1vGTgKC2j/ArbLJKBoYplIuCiePgfL530hgoMoOPupTFcVwAyNFXwv3I70y2/75YA==
Encoding URI, headers and body to utf-8.

@ib-lundgren
Copy link
Member

Thanks. The fact that you can get credentials rules out issues with your ID/keys and everything points towards something wrong with how the hash is integrated. Which is a bit odd since the base string looks ok. I will look at their Java SDK and see if I can figure out where the mismatch is.

@ib-lundgren
Copy link
Member

Looked a bit closer at the base signing string now and a few things popped out

  1. It should not include callback (only needed when obtaining token)
  2. It should not include token (they want it only in the xml body for some reason)
  3. It seems like your url has an extra / after .com, e.g. https://sandbox.api.mastercard.com//online/v1/shopping-cart

Try changing to

def get_session(credentials):
    credentials = get_credentials()
    return OAuth1Session(
        consumer_key,
        signature_method='RSA-SHA1',
        signature_type='auth_header',
        rsa_key=key,
        client_class=BodyHashClient,
        force_include_body=True
    )

@spaceexperiment
Copy link
Author

Thank @ib-lundgren
Fixing the url and removing the token from header and putting it in the xml body fixed the problem.
I am now getting 500 server error.
but that is probably not due to an error in the request through the library, because if a submit a wrong token in the body i get a token error, chances are it's parsing the header and body fine.
I have emailed them with the issue, and will keep yo posted on how it goes.

many thanks for your help

@ib-lundgren
Copy link
Member

Great :)

Their little checklist for 500 errors just mention

If you get "Error 500" when calling MasterPass web services
Verify oauth_body_hash exists and is correct (Post Transaction call only)
Verify Content-Type HTTP header is being sent
Verify correct private key
Verify signature is readable (example, encoded incorrectly)

Are you setting content type to xml? e.g. "application/xml"?

resp = oauth.post(url=shopping_cart_url, data=xml, headers={"Content-Type": "application/xml"})

Does not seem like it from the log above and looks like they require it.

@spaceexperiment
Copy link
Author

Thanks for the reminder
Yes i have set the content-type header to application/xml after i got an error saying it couldn't parse header.
Private key and signature is probably correct as we are able to request token.
It is parsing the token from the body, so there is probably no authentication error or deformed data.
Might be that something should be configured for the sandbox mode or any config for that matter.
Waiting for their reply to assist with this.

@spaceexperiment
Copy link
Author

Turns out there was a problem with the encoding of the xml, was using "utf8" instead of "UTF-8".
Other than that i am able to access the masterpass oauth after using your fix @ib-lundgren .
Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants