Skip to content

Commit

Permalink
Merge pull request #80 from timmygee/feature/public-oauth-flow
Browse files Browse the repository at this point in the history
Add pyxero auth workflow example
  • Loading branch information
aidanlister committed Jul 1, 2015
2 parents b20deea + 0805506 commit e615398
Show file tree
Hide file tree
Showing 8 changed files with 362 additions and 6 deletions.
23 changes: 23 additions & 0 deletions examples/partner_oauth_flow/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
Simple example for the oAuth partner workflow
=============================================

Once you've gone through Xero's process of registering a partner application, having the application
be approved, and obtaining the certificates from Entrust you will need to split the p12 file to use
it with pyxero.

Instructions for doing this are under "Using OpenSSL to split the Xero Entrust certificate":

http://developer.xero.com/documentation/getting-started/partner-applications/

Once done simply run:

XERO_CONSUMER_KEY=yourkey \
XERO_CONSUMER_SECRET=yoursecret \
XERO_RSA_CERT_KEY_PATH=privatekey.pem \
XERO_ENTRUST_CERT_PATH=entrust-cert.pem \
XERO_ENTRUST_PRIVATE_KEY_PATH=entrust-private-nopass.pem \
python runserver.py

Then open your browser and go to http://localhost:8000/


11 changes: 11 additions & 0 deletions examples/partner_oauth_flow/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Example oAuth Flow</title>
</head>

<body>
<a href="/do-auth"><img src="http://xerodev.wpengine.com/wp-content/uploads/2011/09/connect_xero_button_blue1.png"></a>
</body>
</html>
168 changes: 168 additions & 0 deletions examples/partner_oauth_flow/runserver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import sys
import os
import SimpleHTTPServer
import SocketServer
from urlparse import urlparse, parse_qsl

from StringIO import StringIO

from xero.auth import PartnerCredentials
from xero.exceptions import XeroException
from xero import Xero

PORT = 8000


# You should use redis or a file based persistent
# storage handler if you are running multiple servers.
OAUTH_PERSISTENT_SERVER_STORAGE = {}


class PartnerCredentialsHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
def page_response(self, title='', body=''):
"""
Helper to render an html page with dynamic content
"""
f = StringIO()
f.write('<!DOCTYPE html>\n')
f.write('<html>\n')
f.write('<head><title>{}</title><head>\n'.format(title))
f.write('<body>\n<h2>{}</h2>\n'.format(title))
f.write('<div class="content">{}</div>\n'.format(body))
f.write('</body>\n</html>\n')
length = f.tell()
f.seek(0)
self.send_response(200)
encoding = sys.getfilesystemencoding()
self.send_header("Content-type", "text/html; charset=%s" % encoding)
self.send_header("Content-Length", str(length))
self.end_headers()
self.copyfile(f, self.wfile)
f.close()

def redirect_response(self, url, permanent=False):
"""
Generate redirect response
"""
if permanent:
self.send_response(301)
else:
self.send_response(302)
self.send_header("Location", url)
self.end_headers()

def do_GET(self):
"""
Handle GET request
"""
consumer_key = os.environ.get('XERO_CONSUMER_KEY')
consumer_secret = os.environ.get('XERO_CONSUMER_SECRET')
private_key_path = os.environ.get('XERO_RSA_CERT_KEY_PATH')
entrust_cert_path = os.environ.get('XERO_ENTRUST_CERT_PATH')
entrust_private_key_path = os.environ.get('XERO_ENTRUST_PRIVATE_KEY_PATH')

if consumer_key is None or consumer_secret is None:
raise ValueError(
'Please define both XERO_CONSUMER_KEY and XERO_CONSUMER_SECRET environment variables')

if not private_key_path:
raise ValueError(
'Use the XERO_RSA_CERT_KEY_PATH env variable to specify the path to your RSA '
'certificate private key file')

if not entrust_cert_path:
raise ValueError(
'Use the XERO_ENTRUST_CERT_PATH env variable to specify the path to your Entrust '
'certificate')

if not entrust_private_key_path:
raise ValueError(
'Use the XERO_ENTRUST_PRIVATE_KEY_PATH env variable to specify the path to your '
'Entrust private no-pass key')

with open(private_key_path, 'r') as f:
rsa_key = f.read()
f.close()

client_cert = (entrust_cert_path, entrust_private_key_path)

print("Serving path: {}".format(self.path))
path = urlparse(self.path)

if path.path == '/do-auth':
client_cert = (entrust_cert_path, entrust_private_key_path)
credentials = PartnerCredentials(
consumer_key, consumer_secret, rsa_key, client_cert,
callback_uri='http://localhost:8000/oauth')

# Save generated credentials details to persistent storage
for key, value in credentials.state.items():
OAUTH_PERSISTENT_SERVER_STORAGE.update({key: value})

# Redirect to Xero at url provided by credentials generation
self.redirect_response(credentials.url)
return

elif path.path == '/oauth':
params = dict(parse_qsl(path.query))
if 'oauth_token' not in params or 'oauth_verifier' not in params or 'org' not in params:
self.send_error(500, message='Missing parameters required.')
return

stored_values = OAUTH_PERSISTENT_SERVER_STORAGE
client_cert = (entrust_cert_path, entrust_private_key_path)
stored_values.update({'rsa_key': rsa_key, 'client_cert': client_cert})

credentials = PartnerCredentials(**stored_values)

try:
credentials.verify(params['oauth_verifier'])

# Resave our verified credentials
for key, value in credentials.state.items():
OAUTH_PERSISTENT_SERVER_STORAGE.update({key: value})

except XeroException as e:
self.send_error(500, message='{}: {}'.format(e.__class__, e.message))
return

# Once verified, api can be invoked with xero = Xero(credentials)
self.redirect_response('/verified')
return

elif path.path == '/verified':

stored_values = OAUTH_PERSISTENT_SERVER_STORAGE
stored_values.update({'rsa_key': rsa_key, 'client_cert': client_cert})
credentials = PartnerCredentials(**stored_values)

# Partner credentials expire after 30 minutes. Here's how to re-activate on expiry
if credentials.expired():
credentials.refresh()

try:
xero = Xero(credentials)

except XeroException as e:
self.send_error(500, message='{}: {}'.format(e.__class__, e.message))
return

page_body = 'Your contacts:<br><br>'

contacts = xero.contacts.all()

if contacts:
page_body += '<br>'.join([str(contact) for contact in contacts])
else:
page_body += 'No contacts'
self.page_response(title='Xero Contacts', body=page_body)
return

SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)


if __name__ == '__main__':
httpd = SocketServer.TCPServer(("", PORT), PartnerCredentialsHandler)

print "serving at port", PORT
httpd.serve_forever()
17 changes: 17 additions & 0 deletions examples/partner_oauth_flow/verified.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Access Verified</title>
</head>

<body>
<p>
Access verified! Acquire Xero API object with the following code:
</p>
<code>
from xero.api import Xero
xero = Xero(credentials)
</code>
</body>
</html>
2 changes: 1 addition & 1 deletion examples/public_oauth_flow/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ Simply run:

python runserver.py

Then open your browser and go to http://127.0.0.1:8000/
Then open your browser and go to http://localhost:8000/

2 changes: 1 addition & 1 deletion examples/public_oauth_flow/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
</head>

<body>
<a href=""><img src="http://xerodev.wpengine.com/wp-content/uploads/2011/09/connect_xero_button_blue1.png"></a>
<a href="/do-auth"><img src="http://xerodev.wpengine.com/wp-content/uploads/2011/09/connect_xero_button_blue1.png"></a>
</body>
</html>
128 changes: 124 additions & 4 deletions examples/public_oauth_flow/runserver.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,133 @@
import sys
import os
import SimpleHTTPServer
import SocketServer
from urlparse import urlparse, parse_qsl

from StringIO import StringIO

from xero.auth import PublicCredentials
from xero.exceptions import XeroException
from xero import Xero

PORT = 8000

Handler = SimpleHTTPServer.SimpleHTTPRequestHandler

httpd = SocketServer.TCPServer(("", PORT), Handler)
# You should use redis or a file based persistent
# storage handler if you are running multiple servers.
OAUTH_PERSISTENT_SERVER_STORAGE = {}


class PublicCredentialsHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
def page_response(self, title='', body=''):
"""
Helper to render an html page with dynamic content
"""
f = StringIO()
f.write('<!DOCTYPE html">\n')
f.write('<html>\n')
f.write('<head><title>{}</title><head>\n'.format(title))
f.write('<body>\n<h2>{}</h2>\n'.format(title))
f.write('<div class="content">{}</div>\n'.format(body))
f.write('</body>\n</html>\n')
length = f.tell()
f.seek(0)
self.send_response(200)
encoding = sys.getfilesystemencoding()
self.send_header("Content-type", "text/html; charset=%s" % encoding)
self.send_header("Content-Length", str(length))
self.end_headers()
self.copyfile(f, self.wfile)
f.close()

def redirect_response(self, url, permanent=False):
"""
Generate redirect response
"""
if permanent:
self.send_response(301)
else:
self.send_response(302)
self.send_header("Location", url)
self.end_headers()

def do_GET(self):
"""
Handle GET request
"""
consumer_key = os.environ.get('XERO_CONSUMER_KEY')
consumer_secret = os.environ.get('XERO_CONSUMER_SECRET')

if consumer_key is None or consumer_secret is None:
raise KeyError(
'Please define both XERO_CONSUMER_KEY and XERO_CONSUMER_SECRET environment variables')

print("Serving path: {}".format(self.path))
path = urlparse(self.path)

if path.path == '/do-auth':
credentials = PublicCredentials(
consumer_key, consumer_secret, callback_uri='http://localhost:8000/oauth')

# Save generated credentials details to persistent storage
for key, value in credentials.state.items():
OAUTH_PERSISTENT_SERVER_STORAGE.update({key: value})

# Redirect to Xero at url provided by credentials generation
self.redirect_response(credentials.url)
return

elif path.path == '/oauth':
params = dict(parse_qsl(path.query))
if 'oauth_token' not in params or 'oauth_verifier' not in params or 'org' not in params:
self.send_error(500, message='Missing parameters required.')
return

stored_values = OAUTH_PERSISTENT_SERVER_STORAGE
credentials = PublicCredentials(**stored_values)

try:
credentials.verify(params['oauth_verifier'])

# Resave our verified credentials
for key, value in credentials.state.items():
OAUTH_PERSISTENT_SERVER_STORAGE.update({key: value})

except XeroException as e:
self.send_error(500, message='{}: {}'.format(e.__class__, e.message))
return

# Once verified, api can be invoked with xero = Xero(credentials)
self.redirect_response('/verified')
return

elif path.path == '/verified':
stored_values = OAUTH_PERSISTENT_SERVER_STORAGE
credentials = PublicCredentials(**stored_values)

try:
xero = Xero(credentials)

except XeroException as e:
self.send_error(500, message='{}: {}'.format(e.__class__, e.message))
return

page_body = 'Your contacts:<br><br>'

contacts = xero.contacts.all()

if contacts:
page_body += '<br>'.join([str(contact) for contact in contacts])
else:
page_body += 'No contacts'
self.page_response(title='Xero Contacts', body=page_body)
return

SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)

print "serving at port", PORT
httpd.serve_forever()

if __name__ == '__main__':
httpd = SocketServer.TCPServer(("", PORT), PublicCredentialsHandler)

print "serving at port", PORT
httpd.serve_forever()
17 changes: 17 additions & 0 deletions examples/public_oauth_flow/verified.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Access Verified</title>
</head>

<body>
<p>
Access verified! Acquire Xero API object with the following code:
</p>
<code>
from xero.api import Xero
xero = Xero(credentials)
</code>
</body>
</html>

0 comments on commit e615398

Please sign in to comment.