Skip to content
This repository has been archived by the owner on Mar 4, 2022. It is now read-only.

Added support for RSA-SHA1 signature method. #3

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 83 additions & 5 deletions README.rst
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
httpie-oauth
===========
============

OAuth plugin for `HTTPie <https://httpie.org/>`_.

It currently provides support for OAuth 1.0a 2-legged.
OAuth 1.0a two-legged plugin for `HTTPie <https://httpie.org/>`_.


Installation
Expand All @@ -14,16 +12,83 @@ Installation
$ pip install httpie-oauth


You should now see ``oauth1`` under ``--auth-type`` in ``$ http --help`` output.
You should now see ``oauth1`` under ``--auth-type`` in the
``$ http --help`` output.

To be able to use the RSA-SHA1 signature type, also install **PyJWT**
and PyCA's **cryptography** package.

.. code-block:: bash

$ pip install pyjwt
$ pip install cryptography

On CentOS 7, it might be easier to use *yum* to install "epel-release"
and then the "python2-cryptography" packages, since to *pip install* it
requires C code to be compiled.

Usage
-----

HMAC-SHA1
.........

To use the HMAC-SHA1 signature method, in the ``--auth`` parameter
provide the client-key, a single colon and the client-secret.

.. code-block:: bash

$ http --auth-type=oauth1 --auth='client-key:client-secret' example.org

It will interactively prompt for the client-secret, if there is no colon.
If the password starts with a colon, use this interactive method to enter it
(otherwise the extra colon will cause it to use RSA-SHA1 instead of HMAC-SHA1).

RSA-SHA1
........

To use the RSA-SHA1 signature method, in the ``--auth`` parameter
provide the client key, two colons and the name of a file containing
the RSA private key. The file must contain a PEM formatted RSA private
key.

.. code-block:: bash

$ http --auth-type=oauth1 --auth='client-key::filename' example.org

It will interactively prompt for the filename, if there is no value
after the two colons.

The filename can also be a relative or absolute path to the file.

Passphrase protected private keys are not supported.

Including the client key in the private key file
++++++++++++++++++++++++++++++++++++++++++++++++

If the client key in the ``--auth`` parameter is empty (i.e. the
option argument is just two colons and the filename), the
``oauth_consumer_key`` parameter from the file is used. It must
appear in the file before the private key.

For example, if the private key file contains something like this:

::

oauth_consumer_key: myconsumerkey
-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----

It can be used with this command:

.. code-block:: bash

$ http --auth-type=oauth1 --auth=::filename example.org


HTTPie Sessions
...............

You can also use `HTTPie sessions <https://httpie.org/doc#sessions>`_:

Expand All @@ -35,3 +100,16 @@ You can also use `HTTPie sessions <https://httpie.org/doc#sessions>`_:
# Re-use auth
$ http --session=logged-in POST example.org hello=world


Troubleshooting
...............

ImportError: No module named jwt.algorithms
+++++++++++++++++++++++++++++++++++++++++++

The *PyJWT* module is not available. Please install it.

AttributeError: 'module' object has no attribute 'RSAAlgorithm'
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

The PyCA's *cryptography* module is not available. Please install it.
166 changes: 160 additions & 6 deletions httpie_oauth.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
"""
OAuth plugin for HTTPie.
OAuth 1.0a 2-legged plugin for HTTPie.

Supports HMAC-SHA1 and RSA-SHA1 signature methods.

If the authentication parameter is "username:password" then HMAC-SHA1 is used.
If the password is omitted (i.e. --auth username is provided), the user is
prompted for the password.

If the authentication parameter is "username::filename" (double colon between
the username and the filename) then RSA-SHA1 is used, and the PEM formatted
private key is read from that file. If the filename is omitted
(i.e. --auth username:: is provided), the user is prompted for
the filename. The username is used as the oauth_client_key OAuth parameter.

"""
import string
import sys
from httpie.plugins import AuthPlugin
from requests_oauthlib import OAuth1
from oauthlib.oauth1 import SIGNATURE_RSA


__version__ = '1.0.2'
__version__ = '2.0.0'
__author__ = 'Jakub Roztocil'
__licence__ = 'BSD'

Expand All @@ -15,7 +29,147 @@ class OAuth1Plugin(AuthPlugin):

name = 'OAuth 1.0a 2-legged'
auth_type = 'oauth1'
description = ''
description =\
'--auth user:HMAC-SHA1_secret or --auth user::RSA-SHA1_privateKeyFile'

def get_auth(self, username=None, password=None):
"""
Generate OAuth 1.0a 2-legged authentication for HTTPie.

Note: Passpharse protected private keys are not yet supported.
Before support can be implemented, the PyJWT, oauthlib and
requests_oauthlib modules need to be updated.
The passphrase needs to be obtained here and passed
through to PyJWT's jwt/algorithms.py, line 168, where currently it
passes into load_pem_private_key a hardcoded value of None for the
password.
To get it to there, many places in oauthlib's oauth1/rfc5849/__init__.py
and oauth1/rfc5849/signature.py, as well as in requests_oauthlib's
oauth1_auth.py, need to be updated to pass it through.

:param username: username
:param password: password, or colon followed by a filename
:return: requests_oauthlib.oauth1_auth.OAuth1 object
"""
if not password.startswith(':'):
# HMAC-SHA1 signature method (--auth client-key:client-secret)
return OAuth1(client_key=username, client_secret=password)

else:
# RSA-SHA1 signature method (--auth oauth_consumer_key::filename)
filename = password[1:]
if len(filename) == 0:
# Prompt for filename of RSA private key
try:
filename = raw_input('http: filename of RSA private key: ')
except EOFError: # if ^D entered
sys.exit(1)

username, key = OAuth1Plugin.read_private_key(username, filename)

return OAuth1(client_key=username,
signature_method=SIGNATURE_RSA,
rsa_key=key)

@staticmethod
def read_private_key(username, filename):
"""
Check if the key is a recognised private key format.

Prints an error message to stderr and exits if it is not. Uses
crude checks to try and generate more useful error messages.

:param username: username to use
:param filename: file to read private key from
:return: PEM formatted private key
"""
PEM_PRIVATE_KEY_BEGINNING = '-----BEGIN RSA PRIVATE KEY-----'
PEM_PRIVATE_KEY_ENDING = '-----END RSA PRIVATE KEY-----'
ATTR_NAME = 'oauth_consumer_key'

try:
data = open(filename).read()

key_start = data.find(PEM_PRIVATE_KEY_BEGINNING)
key_end = data.find(PEM_PRIVATE_KEY_ENDING)

if key_start == -1:
# Did not find the start of private key.

if data.find('-----BEGIN PUBLIC KEY-----') != -1 or \
data.find('-----BEGIN RSA PUBLIC KEY-----') != -1 or \
data.find('ssh-rsa ') != -1 or \
data.find('---- BEGIN SSH2 PUBLIC KEY ----') != -1:
# Appears to contain a PKCS8, PEM, OpenSSH old or
# OpenSSH new format public key.
# The newer OpenSSH format does not follow RFC7468
# and only has 4 hyphens and spaces around the text!
err = 'wrong key, please provide the PRIVATE key'
elif data.find('-----BEGIN OPENSSH PRIVATE KEY-----') != -1:
# Appears to contain newer OpenSSH private key format
err = 'private key format not supported' + \
', PEM format required'
else:
# Generic error message
err = 'does not contain a PEM formatted private key'

else:
if key_end == -1:
err = 'private key is incomplete'
else:
key_end += len(PEM_PRIVATE_KEY_ENDING)
err = None

# If the username is blank, try to extract a username from the file

if len(username) == 0 and err is None:
username, err = OAuth1Plugin.extract_username(data, ATTR_NAME,
0, key_start)

if err is not None:
sys.stderr.write("http: " + filename + ': ' + err)
sys.exit(1)

pem_key = data[key_start:key_end]

return username, pem_key

except IOError as e:
sys.stderr.write("http: " + str(e))
sys.exit(1)

@staticmethod
def extract_username(data, attr_name, start, end, limit=8096):
"""
Extract a named parameter from the contents.

:param data: text to search
:param attr_name: name of attribute to look for
:param start: index into contents of where to start looking
:param end: index into contents of where to stop looking
:param limit: upper limit for end position
:return: client-key value or None if not found
"""
stop_pos = end
if limit < end:
stop_pos = limit
i = start

while i < stop_pos:
eol_pos = data.find('\n', i, stop_pos)
if eol_pos == -1:
break # no more complete lines found

colon_pos = data.find(':', i, eol_pos)
if colon_pos != -1:
name = data[i:colon_pos].strip()
value = data[colon_pos + 1: eol_pos].strip()
if name == attr_name:
return value, None # successfully found
i = eol_pos + 1

def get_auth(self, username, password):
return OAuth1(client_key=username, client_secret=password)
if limit < end:
return None, '"{}" not found in first {} characters'.format(
attr_name, limit)
else:
return None, '"{}" not found before private key'.format(attr_name)
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
name='httpie-oauth',
description='OAuth plugin for HTTPie.',
long_description=open('README.rst').read().strip(),
version='1.0.2',
version='2.0.0',
author='Jakub Roztocil',
author_email='[email protected]',
license='BSD',
Expand Down