-
Notifications
You must be signed in to change notification settings - Fork 208
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #80 from timmygee/feature/public-oauth-flow
Add pyxero auth workflow example
- Loading branch information
Showing
8 changed files
with
362 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/ | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |