Modified:
trunk/janrain-python/janrain-python.py
Log:
improved consumer support. disco, assoc, and full flow (in stateless
mode) work
now. stateful full flow was working, but isn't any more. need to debug. :/
also updated to conform to new spec at
http://code.google.com/p/openid-test/wiki/TestSpec. supports /caps and
the new
consumer query parameters.
Modified: trunk/janrain-python/janrain-python.py
==============================================================================
--- trunk/janrain-python/janrain-python.py (original)
+++ trunk/janrain-python/janrain-python.py Wed Jan 23 01:35:54 2008
@@ -4,7 +4,8 @@
A test OpenID consumer and provider based on JanRain's Python OpenID library,
version 2.1.1, from http://www.openidenabled.com/python-openid/.
-See http://code.google.com/p/openid-test/.
+See http://code.google.com/p/openid-test/ and
+http://code.google.com/p/openid-test/wiki/TestSpec.
Responds to the following URLs:
@@ -36,6 +37,7 @@
import re
import sys
import traceback
+import urllib
import urlparse
import wsgiref.handlers
import wsgiref.util
@@ -48,39 +50,48 @@
logging.getLogger().addHandler(logging.StreamHandler())
logging.getLogger().setLevel(logging.DEBUG)
-# Set to True if stack traces should be shown in the browser, etc.
-_DEBUG = False
-
-# the global openid server and consumer instances
-oidserver = None
-oidconsumer = None
-
# supported openid protocol versions
PROTOCOL_VERSIONS = ('1.1', '2.0', '1.1,2.0')
+# trust root (realm) to send to provider when acting as consumer
+TRUST_ROOT = 'http://' + os.environ['SERVER_NAME']
+if os.environ['SERVER_PORT'] != '80':
+ TRUST_ROOT += ':' + os.environ['SERVER_PORT']
+
+# base url, usually the URI path to this script with a trailing /
+if 'SCRIPT_NAME' in os.environ:
+ BASE_URL = TRUST_ROOT + os.environ['SCRIPT_NAME'] + '/'
+else:
+ # default
+ BASE_URL = TRUST_ROOT + '/sandbox/cgi-bin/janrain-python.cgi/'
+
+
# regex used to parse URL paths
PATH_REGEX = re.compile(
- """(%s)/
+ """(caps |
+ (%s)/
(identity/will-sign |
identity/will-setup-then-sign |
identity/will-setup-then-cancel |
- rp) $
+ rp |
+ rp\.return_to)
+ )$
""" % '|'.join(PROTOCOL_VERSIONS),
re.VERBOSE)
-# HTML templates and other stuff
-HTML_CONTENT_TYPE = 'text/html; charset=UTF-8'
-# TEXT_CONTENT_TYPE = 'text/plain; charset=UTF-8'
+# HTML templates
DOCTYPE = """<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
"""
HEADER = """<html><head>
-<link rel="openid.server" href="%(uri)s" />
+<link rel="openid.server" href="%%(uri)s" />
+<base href="%s" />
</head>
<body>
-"""
+""" % BASE_URL
+
FOOTER = '</body></html>'
ERROR = """
@@ -92,28 +103,49 @@
ENDPOINT = """
<p>Hi! This is a test OpenID endpoint. You can use this URL as an OpenID
identifier in a OpenID consumer site:</p>
-<p><i>%(uri)s</i></p>
+<p><i><a href="%(uri)s">%(uri)s</a></i></p>
"""
CONSUMER_FORM = """
<form method='get'>
<label for='openid_identifier'> OpenID URL to test: </label>
<input type='text' size=50 name='openid_identifier' />
- <input type=submit value='test' />
- <div class='options'>
- <input type='checkbox' value='1' name='discovery_only'
id='discover_only' />
- <label for='discover_only'>Only do fetch/discovery, then stop.
Don't check id.</label><br/>
- <input type='checkbox' value='1' name="stateless" id='stateless'>
- <label for='stateless'>Stateless mode: don't associate.</label>
- <br/>
- </div>
+ <input type='submit' value='Go' />
+
+ <table><tr><td>
+ Operation:<br />
+ <input type='radio' name='op' value='' id='op_everything' checked />
+ <label for='op_everything'>Everything (full OpenID
flow)</label><br />
+ <input type='radio' name='op' value='disco' id='op_disco' />
+ <label for='op_disco'>Return discovery information</label><br />
+ <input type='radio' name='op' value='assoc' id='op_assoc' />
+ <label for='op_assoc'>Return association details</label>
+ </td><td>
+ Association mode:<br />
+ <input type='radio' name='assoc_mode' value='stateful'
id='stateful' checked />
+ <label for='stateful'>Stateful, possibly cached</label><br />
+ <input type='radio' name='assoc_mode' value='stateful_new'
id='stateful_new' />
+ <label for='stateful_new'>Stateful, create new
association</label><br />
+ <input type='radio' name='assoc_mode' value='stateless'
id='stateless' />
+ <label for='stateless'>Don't associate (dumb mode)</label><br />
+ <input type='radio' name='assoc_mode' value='use_handle'
id='use_handle' />
+ <label for='use_handle'>Use association handle:</label>
+ <input type='text' name='assoc_handle' />
+ </td></tr></table>
</form>
"""
+OK = 'OK\n'
+
DISCOVERY = """
-claimed_url: %(claimed_url)s
+user_specified_url: %(claimed_id)s
openid_provider: %(openid_provider)s
-delegated_url: %(delegated_url)s
+local_id: %(local_id)s
+"""
+
+ASSOCIATION = """
+handle: %(handle)s
+type: %(type)s
"""
FRONT_PAGE = """
@@ -137,26 +169,27 @@
""" % tuple(
['\n'.join([template % (ver, ver) for ver in PROTOCOL_VERSIONS])
for template in [
- '<li><a href="/cgi-bin/janrain-python.cgi/%s/rp">/%s/rp</a></li>',
- '<li><a href="/cgi-bin/janrain-python.cgi/%s/identity/will-sign">/%s/identity/will-sign</a></li>',
- '<li><a href="/cgi-bin/janrain-python.cgi/%s/identity/will-setup-then-sign">/%s/identity/will-setup-then-sign</a></li>',
- '<li><a href="/cgi-bin/janrain-python.cgi/%s/identity/will-setup-then-cancel">/%s/identity/will-setup-then-cancel</a></li>',
+ '<li><a href="%s/rp">%s/rp</a></li>',
+ '<li><a href="%s/identity/will-sign">%s/identity/will-sign</a></li>',
+ '<li><a href="%s/identity/will-setup-then-sign">%s/identity/will-setup-then-sign</a></li>',
+ '<li><a href="%s/identity/will-setup-then-cancel">%s/identity/will-setup-then-cancel</a></li>',
]])
-
-def InitializeOpenID(endpoint_uri, version):
- global oidserver, oidconsumer
-
- store = FileOpenIDStore('.')
- oidserver = OpenIDServer(store, endpoint_uri)
- # TODO(ryanb): session
- oidconsumer = OpenIDConsumer({}, store)
-
+CAPS = """
+# Based on JanRain's Python OpenID library, version 2.1.1.
+# See http://www.openidenabled.com/python-openid/
+
+#openid1.1
+openid2.0
+# xri
+"""
class Handler:
- """Base handler class."""
+ """Base request handler class."""
def __init__(self, environ, start_response):
self.environ = environ
+ self.uri = wsgiref.util.request_uri(self.environ, include_query=False)
+ self.request = wsgiref.util.request_uri(self.environ, include_query=True)
self.start_response = start_response
self.output = []
@@ -177,7 +210,7 @@
"""
try:
- oidrequest = oidserver.decodeRequest(self.arg_dict())
+ oidrequest = self.openid_server.decodeRequest(self.arg_dict())
if oidrequest:
logging.debug('Received OpenID request: %s' % oidrequest)
else:
@@ -192,7 +225,7 @@
"""Send an OpenID response.
"""
logging.debug('Sending OpenID response: %s' % oidresponse)
- encoded_response = oidserver.encodeResponse(oidresponse)
+ encoded_response = self.openid_server.encodeResponse(oidresponse)
# build the HTTP status code string
message = BaseHTTPServer.BaseHTTPRequestHandler.responses.get(
@@ -209,24 +242,27 @@
self.output = [encoded_response.body]
def render(self, template, message='', status_code='200 OK',
- content_type=HTML_CONTENT_TYPE, extra_values={}):
+ content_type='text/html', data={}):
"""Render the given template, including the extra (optional) values.
"""
self.start_response(status_code, [('Content-Type', content_type)])
form = cgi.FieldStorage()
- values = {
- 'uri': wsgiref.util.request_uri(self.environ, include_query=False),
- 'request': wsgiref.util.request_uri(self.environ, include_query=True),
+ data_dict = {
+ 'uri': self.uri,
+ 'request': self.request,
'form': pprint.pformat(form),
'message': message,
}
- values.update(self.arg_dict())
- values.update(extra_values)
+ data_dict.update(self.arg_dict())
+ data_dict.update(data)
- if content_type == HTML_CONTENT_TYPE:
- self.output = [DOCTYPE, HEADER % values, template % values, FOOTER]
+ if content_type == 'text/html':
+ self.output = [DOCTYPE, HEADER % data_dict, template %
data_dict, FOOTER]
else:
- self.output = [template % values]
+ self.output = [template % data_dict]
+
+ # work around a bug in wsgiref.handlers that makes it not support unicode.
+ self.output = [str(part) for part in self.output]
def get(self):
self.render(ENDPOINT, 'Welcome!')
@@ -246,25 +282,31 @@
class FrontPage(Handler):
"""Displays the front page with all of the supported links."""
def get(self):
- self.render(FRONT_PAGE, '')
+ self.render(FRONT_PAGE)
-class Provider(Handler):
- """The base provider handler."""
+class Capabilities(Handler):
+ """Displays the capabilities page."""
+ def get(self):
+ self.render(CAPS, content_type='text/plain')
+
+class Provider(Handler):
+ """The base OpenID provider request handler class."""
def __init__(self, environ, start_response, checkid_immediate=True,
checkid_setup=True):
"""The keyword argument values determine how we respond to each mode."""
Handler.__init__(self, environ, start_response)
self.checkid_immediate = checkid_immediate
self.checkid_setup = checkid_setup
+ self.openid_server = OpenIDServer(FileOpenIDStore('.'), self.uri)
def post(self):
"""Handles associate and check_authentication requests."""
oidrequest = self.get_openid_request()
if oidrequest:
if oidrequest.mode in ('associate', 'check_authentication'):
- self.respond(oidserver.handleRequest(oidrequest))
+ self.respond(self.openid_server.handleRequest(oidrequest))
else:
self.render(ERROR, 'Expected an OpenID request', '404 Not found')
@@ -275,19 +317,18 @@
self.respond(oidrequest.answer(getattr(self, oidrequest.mode)))
elif oidrequest is False:
Handler.get(self)
-
+
class Sign(Provider):
"""Signs a requests and verifies the signature."""
-
def __init__(self, environ, start_response):
"""The keyword argument values determine how we respond to each mode."""
Provider.__init__(self, environ, start_response, checkid_immediate=False,
checkid_setup=True)
+
class SetupThenSign(Sign):
"""Does setup for a new request, then signs and verifies a signature."""
-
def __init__(self, environ, start_response):
"""The keyword argument values determine how we respond to each mode."""
Provider.__init__(self, environ, start_response, checkid_immediate=False,
@@ -296,7 +337,6 @@
class SetupThenCancel(Sign):
"""Does setup for a new request, then signs it and sends a cancel back."""
-
def __init__(self, environ, start_response):
"""The keyword argument values determine how we respond to each mode."""
Provider.__init__(self, environ, start_response, checkid_immediate=False,
@@ -305,37 +345,89 @@
class Consumer(Handler):
"""The base consumer handler."""
+ def __init__(self, *args):
+ Handler.__init__(self, *args)
+
+ # set up the association store based on the assoc_mode query parameter
+ store = None
+ assoc_mode = self.arg_dict().get('assoc_mode')
+ if assoc_mode:
+ if assoc_mode == 'stateful':
+ store = FileOpenIDStore('.')
+ elif assoc_mode == 'stateful_new':
+ # TODO(ryanb)
+ store = FileOpenIDStore('.')
+ elif assoc_mode == 'stateless':
+ store = None
+ elif assoc_mode == 'use_handle':
+ # TODO(ryanb)
+ pass
+ else:
+ assert False, 'bad assoc_mode value: %s' % assoc_mode
+
+ # note that we don't support persistent session data
+ self.openid_consumer = OpenIDConsumer({}, store)
def get(self):
- """Handles all consumer requests."""
+ """Handles consumer GET requests."""
args = self.arg_dict()
- if 'openid_identifier' not in args:
- # no query; show the form
- self.render(CONSUMER_FORM, 'unused')
- return
+ if 'openid.mode' in args:
+ # this is a redirect from a provider. attempt to verify the login.
+ response = self.openid_consumer.complete(args, self.uri)
+ if response.status == 'success':
+ self.render(OK, content_type='text/plain')
+ else:
+ self.render(ERROR, 'Provider responded: %s' % response.status)
- auth_request = oidconsumer.begin(args['openid_identifier'])
+ elif 'openid_identifier' in args:
+ # start with discovery
+ auth_request = self.openid_consumer.begin(args['openid_identifier'])
+
+ # this is a form post. what to do...?
+ if args.get('op') == 'disco':
+ # return the discovery details
+ claimed_id = auth_request.endpoint.claimed_id
+ local_id = auth_request.endpoint.local_id
+ if local_id == claimed_id:
+ local_id = ''
+
+ discovered = {'claimed_id': claimed_id,
+ 'openid_provider': auth_request.endpoint.server_url,
+ 'local_id': local_id}
+ self.render(DISCOVERY, content_type='text/plain', data=discovered)
+ return
+
+ elif args.get('op') == 'assoc':
+ # return the association we would have used
+ if auth_request.assoc:
+ assoc = {'handle': auth_request.assoc.handle,
+ 'type': auth_request.assoc.assoc_type }
+ else:
+ assoc = {'handle': '', 'type': ''}
- if args.get('discovery_only', None):
- discovered = {'claimed_url': auth_request.endpoint.claimed_id,
- 'openid_provider': auth_request.endpoint.server_url,
- 'delegated_url': auth_request.endpoint.local_id}
- self.render(DISCOVERY, content_type='text/plain', extra_values=discovered)
- return
+ self.render(ASSOCIATION, content_type='text/plain', data=assoc)
- hostname = self.environ['SERVER_NAME']
- return_to = wsgiref.util.request_uri(self.environ)
- redirect_to = auth_request.redirectURL(hostname, return_to)
- self.start_response('302 Found', [('Location', redirect_to)])
+ else:
+ # start the flow!
+ return_to = self.uri + '.return_to'
+ redirect_to = auth_request.redirectURL(TRUST_ROOT, return_to)
+ self.start_response('302 Found', [('Location', redirect_to)])
+
+ else:
+ # this is an initial GET. show the form.
+ self.render(CONSUMER_FORM, 'unused')
+ return
def app(environ, start_response):
"""Pass the request to the right handler."""
- handlers = {'identity/will-sign': Sign,
+ handlers = {'caps': Capabilities,
+ 'identity/will-sign': Sign,
'identity/will-setup-then-sign': SetupThenSign,
'identity/will-setup-then-cancel': SetupThenCancel,
'rp': Consumer,
+ 'rp.return_to': Consumer,
}
uri = wsgiref.util.request_uri(environ, include_query=False)
@@ -346,9 +438,11 @@
handler_cls = FrontPage
if match and method in ('GET', 'POST'):
- openid_version, action = match.groups()
+ # TODO(ryanb); ugh, this is awful. make this not awful.
+ caps, openid_version, action = match.groups()
+ if caps == 'caps':
+ action = 'caps'
if action in handlers:
- InitializeOpenID(uri, openid_version)
handler_cls = handlers[action]
handler = handler_cls(environ, start_response)