# HG changeset patch # User paulb # Date 1194828694 0 # Node ID 26865b172666db0d755f07e22cea55ff086e36d9 # Parent 7f1f02b485f8eb33dc41c070922619abb79e14d9 [project @ 2007-11-12 00:51:34 by paulb] Added a StringResource class for simple static resources. Introduced base classes for common authentication activities. Merged "app", "path" and "qs" fields into a single "app" field for login and redirection. Added support for OpenID authentication. diff -r 7f1f02b485f8 -r 26865b172666 WebStack/Resources/Login.py --- a/WebStack/Resources/Login.py Mon Nov 12 00:50:03 2007 +0000 +++ b/WebStack/Resources/Login.py Mon Nov 12 00:51:34 2007 +0000 @@ -22,7 +22,7 @@ """ import WebStack.Generic -from WebStack.Helpers.Auth import get_token +from WebStack.Helpers.Auth import LoginAuthenticator class LoginResource: @@ -59,57 +59,59 @@ "Respond using the transaction 'trans'." - app, path, qs = get_target(trans, self.urlencoding, self.encoding) - # Check for a submitted login form. fields_body = trans.get_fields_from_body(self.encoding) if fields_body.has_key("login"): if self.authenticator.authenticate(trans, fields_body.get("username", [None])[0], fields_body.get("password", [None])[0]): - self._redirect(trans, app, path, qs) + app = fields_body.get("app", [""])[0] + + self._redirect(trans, app) # The above method does not return. # Otherwise, show the login form. - self.show_login(trans, app, path, qs) + fields_path = trans.get_fields_from_path(self.urlencoding) + app = fields_path.get("app", [""])[0] - def _redirect(self, trans, app, path, qs): + self.show_login(trans, app) + + def _redirect(self, trans, app): """ - Redirect the client using 'trans' and the given 'app', 'path' and 'qs' - details. + Redirect the client using 'trans' and the given 'app' details. """ # Show the success page anyway. - self.show_success(trans, app, path, qs) + self.show_success(trans, app) if self.use_redirect: - trans.redirect(app + trans.encode_path(path, self.urlencoding) + qs) + trans.redirect(app) else: raise WebStack.Generic.EndOfResponse - def show_login(self, trans, app, path, qs): + def show_login(self, trans, app): """ Writes a login screen using the transaction 'trans', including details - of the 'app', 'path' and 'qs' which the client was attempting to access. + of the 'app' which the client was attempting to access. """ trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding)) out = trans.get_response_stream() - out.write(self.login_page % (app, path, qs)) + out.write(self.login_page % app) - def show_success(self, trans, app, path, qs): + def show_success(self, trans, app): """ Writes a success screen using the transaction 'trans', including details - of the 'app', 'path' and 'qs' which the client was attempting to access. + of the 'app' which the client was attempting to access. """ trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding)) out = trans.get_response_stream() - out.write(self.success_page % (app, trans.encode_path(path, self.urlencoding), qs)) + out.write(self.success_page % (app, app)) login_page = """ @@ -123,8 +125,6 @@

Password:

- - @@ -137,101 +137,9 @@

Login Successful

-

Please proceed to the application.

+

Please proceed to the application: %s

""" -class LoginAuthenticator: - - def __init__(self, secret_key, credentials, cookie_name=None): - - """ - Initialise the authenticator with a 'secret_key', the authenticator's registry of - 'credentials' and an optional 'cookie_name'. - - The 'credentials' must be an object which supports tests of the form - '(username, password) in credentials'. - """ - - self.secret_key = secret_key - self.credentials = credentials - self.cookie_name = cookie_name or "LoginAuthenticator" - - def authenticate(self, trans, username, password): - - """ - Authenticate the sender of the transaction 'trans', returning 1 (true) if they are - recognised, 0 (false) otherwise. Use the 'username' and 'password' supplied as - credentials. - """ - - # Process any supplied parameters. - - fields = trans.get_fields_from_body() - - # Check against the class's credentials. - - if (username, password) in self.credentials: - - # Make a special cookie token. - - self.set_token(trans, username) - return 1 - - return 0 - - def set_token(self, trans, username): - - "Set an authentication token in 'trans' with the given 'username'." - - trans.set_cookie_value( - self.cookie_name, - get_token(username, self.secret_key), - path="/" - ) - -# General functions. - -def get_target(trans, urlencoding=None, encoding=None): - - """ - Return the application, path and query string for 'trans' using the optional - 'urlencoding' (or path encoding) and request body 'encoding'. - """ - - fields_path = trans.get_fields_from_path(urlencoding) - fields_body = trans.get_fields_from_body(encoding) - - # NOTE: Handle missing redirects better. - - if fields_body.has_key("app"): - apps = fields_body["app"] - app = apps[0] - elif fields_path.has_key("app"): - apps = fields_path["app"] - app = apps[0] - else: - app = u"" - - if fields_body.has_key("path"): - paths = fields_body["path"] - path = paths[0] - elif fields_path.has_key("path"): - paths = fields_path["path"] - path = paths[0] - else: - path = u"" - - if fields_body.has_key("qs"): - qss = fields_body["qs"] - qs = qss[0] - elif fields_path.has_key("qs"): - qss = fields_path["qs"] - qs = qss[0] - else: - qs = u"" - - return app, path, qs - # vim: tabstop=4 expandtab shiftwidth=4 diff -r 7f1f02b485f8 -r 26865b172666 WebStack/Resources/LoginRedirect.py --- a/WebStack/Resources/LoginRedirect.py Mon Nov 12 00:50:03 2007 +0000 +++ b/WebStack/Resources/LoginRedirect.py Mon Nov 12 00:51:34 2007 +0000 @@ -21,7 +21,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA """ -from WebStack.Helpers.Auth import get_token +from WebStack.Helpers.Auth import Verifier import WebStack.Generic class LoginRedirectResource: @@ -123,16 +123,12 @@ # Redirect to the login URL. - path = trans.get_path_without_query(self.path_encoding) - qs = trans.get_query_string() - if qs: - qs = "?" + qs - trans.redirect("%s?app=%s&path=%s&qs=%s" % ( + path = trans.get_path(self.path_encoding) + trans.redirect("%s?app=%s%s" % ( self.get_login_url(trans), trans.encode_path(self.get_app_url(trans), self.path_encoding), trans.encode_path(path, self.path_encoding), - trans.encode_path(qs, self.path_encoding)) - ) + )) def get_app_url(self, trans): @@ -182,61 +178,38 @@ """ -class LoginRedirectAuthenticator: +class LoginRedirectAuthenticator(Verifier): """ - An authenticator which verifies the credentials provided in a special login cookie. + An authenticator which verifies the credentials provided in a special login + cookie. """ - def __init__(self, secret_key, cookie_name=None): - - "Initialise the authenticator with a 'secret_key' and an optional 'cookie_name'." - - self.secret_key = secret_key - self.cookie_name = cookie_name or "LoginAuthenticator" - def authenticate(self, trans): """ - Authenticate the originator of 'trans', updating the object if successful and - returning 1 (true) if successful, 0 (false) otherwise. + Authenticate the originator of 'trans', updating the object if + successful and returning a true value if successful, or a false value + otherwise. """ - cookie = trans.get_cookie(self.cookie_name) - if cookie is None or cookie.value is None: - return 0 + valid = Verifier.authenticate(self, trans) - # Test the token from the cookie against a recreated token using the - # given information. + # Update the transaction with the user details. - username = cookie.value.split(":")[0] - if cookie.value == get_token(username, self.secret_key): - - # Update the transaction with the user details. - + if valid: + username, token = self.get_username_and_token(trans) trans.set_user(username) - return 1 - else: - return 0 + return valid def set_token(self, trans, username): "Set an authentication token in 'trans' with the given 'username'." - trans.set_cookie_value( - self.cookie_name, - get_token(username, self.secret_key), - path="/" - ) + Verifier.set_token(self, trans, username) # Update the transaction with the user details. trans.set_user(username) - def unset_token(self, trans): - - "Unset the authentication token in 'trans'." - - trans.delete_cookie(self.cookie_name) - # vim: tabstop=4 expandtab shiftwidth=4 diff -r 7f1f02b485f8 -r 26865b172666 WebStack/Resources/OpenIDInitiation.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebStack/Resources/OpenIDInitiation.py Mon Nov 12 00:51:34 2007 +0000 @@ -0,0 +1,223 @@ +#!/usr/bin/env python + +""" +OpenID initiation resources which redirect clients to an OpenID provider. + +Copyright (C) 2004, 2005, 2006, 2007 Paul Boddie + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA +""" + +import WebStack.Generic +import libxml2dom + +class OpenIDInitiationResource: + + "A resource providing an OpenID initiation screen." + + encoding = "utf-8" + openid_ns = "http://specs.openid.net/auth/2.0" + + def __init__(self, openid_mode=None, use_redirect=1, urlencoding=None, encoding=None): + + """ + Initialise the resource. + + The optional 'openid_mode' parameter may be set to "checkid_immediate" + or "checkid_setup" (the default). + + If the optional 'use_redirect' flag is set to 0, a confirmation screen + is given instead of redirecting the user back to the original + application. + + The optional 'urlencoding' parameter allows a special encoding to be + used in producing the redirection path. + + The optional 'encoding' parameter allows a special encoding to be used + in producing the initiation pages. + + To change the pages employed by this resource, either redefine the + 'initiation_page' and 'success_page' attributes in instances of this class or + a subclass, or override the 'show_initiation' and 'show_success' methods. + """ + + self.openid_mode = openid_mode or "checkid_setup" + self.use_redirect = use_redirect + self.urlencoding = urlencoding + self.encoding = encoding or self.encoding + + def respond(self, trans): + + "Respond using the transaction 'trans'." + + app = get_target(trans, self.urlencoding, self.encoding) + + # Check for a submitted initiation form. + + fields_body = trans.get_fields_from_body(self.encoding) + + if fields_body.has_key("initiate") and fields_body.has_key("identity"): + claimed_identifier, provider, local_identifier = self.get_provider_url(fields_body["identity"][0]) + if provider is not None: + self._redirect(trans, app, claimed_identifier, provider, local_identifier) + # The above method does not return. + + # Otherwise, show the initiation form. + + self.show_initiation(trans, app) + + def _redirect(self, trans, app, claimed_identifier, provider, local_identifier): + + """ + Redirect the client using 'trans' and the given 'app', + 'claimed_identifier', 'provider' and 'local_identifier' details. + + See: + http://openid.net/specs/openid-authentication-2_0-12.html#rfc.section.5.2 + http://openid.net/specs/openid-authentication-2_0-12.html#rfc.section.9 + """ + + # NOTE: Should consider the special "select" mode for identity. + + url = "%s?openid.ns=%s&openid.mode=%s&openid.return_to=%s&openid.claimed_id=%s&openid.identity=%s" % ( + provider, + trans.encode_path(self.openid_ns, self.urlencoding), + trans.encode_path(self.openid_mode, self.urlencoding), + trans.encode_path(app, self.urlencoding), + trans.encode_path(claimed_identifier, self.urlencoding), + trans.encode_path(local_identifier, self.urlencoding) + ) + + # Show the success page anyway. + + self.show_success(trans, url) + + # Redirect to the OpenID provider URL. + + if self.use_redirect: + trans.redirect(url) + else: + raise WebStack.Generic.EndOfResponse + + def get_provider_url(self, identity): + + """ + Return the claimed identifier, provider URL and local identifier for the + authenticating user using the given 'identity'. + + See: + http://openid.net/specs/openid-authentication-2_0-12.html#rfc.section.7.3 + """ + + if identity.startswith("xri://"): + identity = openid[6:] + + # NOTE: Not yet discovering XRI providers. + + if identity[0] in ("=", "@", "+", "$", "!", "("): + pass + else: + if not identity.startswith("http"): + identity = "http://" + identity + + # Obtain a provider url from a resource at the stated URL. + + doc = libxml2dom.parseURI(identity, html=1) + provider_links = doc.xpath("/html/head/link[contains(@rel, 'openid2.provider')]/@href") + local_ids = doc.xpath("/html/head/link[contains(@rel, 'openid2.local_id')]/@href") + if provider_links: + if local_ids: + return identity, provider_links[0].nodeValue, local_ids[0].nodeValue + else: + return identity, provider_links[0].nodeValue, None + + return identity, None, None + + def show_initiation(self, trans, app): + + """ + Writes a initiation screen using the transaction 'trans', including details + of the 'app' which the client was attempting to access. + """ + + trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding)) + out = trans.get_response_stream() + out.write(self.initiation_page % app) + + def show_success(self, trans, url): + + """ + Writes a success screen using the transaction 'trans', including details + of the OpenID provider 'url'. + """ + + trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding)) + out = trans.get_response_stream() + out.write(self.success_page % (url, url)) + + initiation_page = """ + + + Authenticate via OpenID + + +

Authenticate via OpenID

+
+

OpenID Identifier (URL):

+

+ +
+ + +""" + + success_page = """ + + + Authenticate via OpenID + + +

Authenticate via OpenID

+

Please proceed to the OpenID provider: %s.

+ + +""" + +# General functions. + +def get_target(trans, urlencoding=None, encoding=None): + + """ + Return the application for 'trans' using the optional 'urlencoding' (or path + encoding) and request body 'encoding'. + """ + + fields_path = trans.get_fields_from_path(urlencoding) + fields_body = trans.get_fields_from_body(encoding) + + # NOTE: Handle missing redirects better. + + if fields_body.has_key("app"): + apps = fields_body["app"] + app = apps[0] + elif fields_path.has_key("app"): + apps = fields_path["app"] + app = apps[0] + else: + app = u"" + + return app + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r 7f1f02b485f8 -r 26865b172666 WebStack/Resources/OpenIDLogin.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebStack/Resources/OpenIDLogin.py Mon Nov 12 00:51:34 2007 +0000 @@ -0,0 +1,247 @@ +#!/usr/bin/env python + +""" +OpenID provider login resources which redirect clients back to the application +("relying party"). + +Copyright (C) 2004, 2005, 2006, 2007 Paul Boddie + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA +""" + +import WebStack.Generic +from WebStack.Helpers.Auth import Authenticator, check_openid_signature, make_openid_signature +import datetime +import time +import random + +class OpenIDLoginResource: + + "A resource providing a login screen." + + encoding = "utf-8" + openid_ns = "http://specs.openid.net/auth/2.0" + + def __init__(self, app_url, authenticator, associations=None, use_redirect=1, urlencoding=None, encoding=None): + + """ + Initialise the resource with the application URL 'app_url' and an + 'authenticator'. + + The optional 'associations' is a mapping from association handles to + secret keys. + + If the optional 'use_redirect' flag is set to 0, a confirmation screen + is given instead of redirecting the user back to the original + application. + + The optional 'urlencoding' parameter allows a special encoding to be + used in producing the redirection path. + + The optional 'encoding' parameter allows a special encoding to be used + in producing the login pages. + + To change the pages employed by this resource, either redefine the + 'login_page' and 'success_page' attributes in instances of this class or + a subclass, or override the 'show_login' and 'show_success' methods. + """ + + self.app_url = app_url + self.authenticator = authenticator + self.associations = associations or {} + self.use_redirect = use_redirect + self.urlencoding = urlencoding + self.encoding = encoding or self.encoding + + def respond(self, trans): + + "Respond using the transaction 'trans'." + + # Check for a submitted login form. + + fields_body = trans.get_fields_from_body(self.encoding) + + if fields_body.has_key("login"): + + # Check a combination of local identifier and username together with + # the password. + + claimed_id = fields_body.get("claimed_id", [""])[0] + local_id = fields_body.get("local_id", [""])[0] + username = fields_body.get("username", [""])[0] + password = fields_body.get("password", [""])[0] + app = fields_body.get("app", [""])[0] + + if self.authenticator.authenticate(trans, (local_id, username), password): + self._redirect(trans, claimed_id, local_id, username, app) + # The above method does not return. + + # Check for an OpenID signature verification request. + + elif fields_body.get("openid.mode", [None])[0] == "check_authentication": + + # Obtain the secret key from recorded associations. + + handle = fields_body.get("openid.assoc_handle", [None])[0] + if handle is not None and self.associations.has_key(handle): + valid = check_openid_signature(fields_body, self.associations[handle]) + del self.associations[handle] + else: + valid = 0 + + # Produce a response for this request. + + self.show_verification(trans, valid) + # The above method does not return. + + # Otherwise, show the login form. + + fields_path = trans.get_fields_from_path(self.urlencoding) + app = fields_path.get("openid.return_to", [""])[0] + claimed_id = fields_path.get("openid.claimed_id", [""])[0] + local_id = fields_path.get("openid.identity", [""])[0] + + self.show_login(trans, app, claimed_id, local_id) + + def _redirect(self, trans, claimed_id, local_id, username, app): + + """ + Redirect the client using 'trans', 'claimed_id', 'local_id', 'username' + and the given 'app' details. + """ + + app_url = self.app_url + trans.get_path_without_query(self.urlencoding) + + # Make an association that can be used in signature verification. + # NOTE: Probably need to consider the secret key a bit more. + + handle = username + str(time.time()) + secret_key = str(random.randint(0, 999999999)) + self.associations[handle] = secret_key + + # Make a timestamp. + + now = datetime.datetime.utcnow() + timestamp = now.strftime("%Y-%m-%dT%H:%M:%SZ") + str(now.microsecond) + + # Make a signature. + + signed_names = ["op_endpoint", "return_to", "response_nonce", "assoc_handle", "claimed_id", "identity"] + fields = { + "openid.op_endpoint" : [app_url], + "openid.return_to" : [app], + "openid.response_nonce" : [timestamp], + "openid.assoc_handle" : [handle], + "openid.claimed_id" : [claimed_id], + "openid.identity" : [local_id] + } + + signature = make_openid_signature(signed_names, fields, secret_key) + + # Build an URL for returning to the application. + + url = "%s?openid.ns=%s&openid.mode=%s&openid.signed=%s&openid.sig=%s" % ( + app, + trans.encode_path(self.openid_ns, self.urlencoding), + trans.encode_path("id_res", self.urlencoding), + trans.encode_path(",".join(signed_names), self.urlencoding), + trans.encode_path(signature, self.urlencoding) + ) + + for name, value in fields.items(): + url += "&%s=%s" % (name, trans.encode_path(value[0], self.urlencoding)) + + # Show the success page anyway. + + self.show_success(trans, url) + if self.use_redirect: + trans.redirect(url) + else: + raise WebStack.Generic.EndOfResponse + + def show_verification(self, trans, status): + + """ + Writes a signature verification response using the transaction 'trans' + and the 'status' of the verification. + """ + + trans.set_content_type(WebStack.Generic.ContentType("text/plain")) + out = trans.get_response_stream() + + # NOTE: Need to use invalidate_handle, too. + + if status: + status_str = "true" + else: + status_str = "false" + out.write("ns:%s\nis_valid:%s\n" % (self.openid_ns, status_str)) + raise WebStack.Generic.EndOfResponse + + def show_login(self, trans, app, claimed_id, local_id): + + """ + Writes a login screen using the transaction 'trans', including details + of the 'app' which the client was attempting to access, along with the + 'claimed_id' and 'local_id'. + """ + + trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding)) + out = trans.get_response_stream() + out.write(self.login_page % (app, claimed_id, local_id)) + + def show_success(self, trans, app): + + """ + Writes a success screen using the transaction 'trans', including details + of the 'app' which the client was attempting to access. + """ + + trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding)) + out = trans.get_response_stream() + out.write(self.success_page % (app, app)) + + login_page = """ + + + Login + + +

Login

+
+

Username:

+

Password:

+

+ + + +
+ + +""" + + success_page = """ + + + Login Example + + +

Login Successful

+

Please proceed to the application: %s

+ + +""" + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r 7f1f02b485f8 -r 26865b172666 WebStack/Resources/OpenIDRedirect.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebStack/Resources/OpenIDRedirect.py Mon Nov 12 00:51:34 2007 +0000 @@ -0,0 +1,205 @@ +#!/usr/bin/env python + +""" +OpenID redirection classes, sending unauthenticated users to the OpenID +initiation page. + +Copyright (C) 2004, 2005, 2006, 2007 Paul Boddie + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA +""" + +from WebStack.Helpers.Auth import Verifier, check_openid_signature +import WebStack.Generic +import datetime +import urllib + +class OpenIDRedirectAuthenticator(Verifier): + + """ + An authenticator which verifies the credentials provided in a special login + cookie, accepting OpenID assertions if necessary. + """ + + encoding = "utf-8" + openid_ns = "http://specs.openid.net/auth/2.0" + replay_limit = datetime.timedelta(0, 10) # 10s + + def __init__(self, secret_key, app_url, associations=None, replay_limit=None, + cookie_name=None, urlencoding=None): + + """ + Initialise the authenticator with a 'secret_key', 'app_url' and optional + 'associations', 'replay_limit', 'cookie_name' and 'urlencoding'. + """ + + Verifier.__init__(self, secret_key, cookie_name) + + self.app_url = app_url + self.associations = associations or {} + self.replay_limit = replay_limit or self.replay_limit + self.urlencoding = urlencoding or self.encoding + + def authenticate(self, trans): + + """ + Authenticate the originator of 'trans', updating the object if + successful and returning a true value if successful, or a false value + otherwise. + """ + + # First, try to authenticate with an application cookie. + + valid = Verifier.authenticate(self, trans) + + # Without a valid login, attempt to verify OpenID assertions. + # http://openid.net/specs/openid-authentication-2_0-12.html#rfc.section.11 + + if not valid: + fields_path = trans.get_fields_from_path(self.urlencoding) + + if fields_path.get("openid.ns", [None])[0] == self.openid_ns and \ + fields_path.get("openid.mode", [None])[0] == "id_res": + + # NOTE: Could expose all errors. + + try: + + if self.test_url(fields_path) and \ + self.test_signature(fields_path) and \ + self.test_replay(fields_path): + + self.set_token(trans, fields_path["openid.identity"][0]) + + # NOTE: Should return true and let the redirector do this. + trans.redirect(fields_path["openid.return_to"][0]) + #return 1 + + # Incomplete assertion. + + except (KeyError, ValueError): + raise + + # Assertion failed or was incomplete. + + return 0 + + # Update the transaction with the user details. + + if valid: + username, token = self.get_username_and_token(trans) + trans.set_user(username) + return valid + + def test_url(self, fields_path): + + """ + See: + http://openid.net/specs/openid-authentication-2_0-12.html#rfc.section.11.1 + """ + + # NOTE: Currently, this is not strict enough. + + return fields_path["openid.return_to"][0].startswith(self.app_url) + + def test_signature(self, fields_path): + + """ + See: + http://openid.net/specs/openid-authentication-2_0-12.html#rfc.section.11.4 + http://openid.net/specs/openid-authentication-2_0-12.html#rfc.section.6 + """ + + handle = fields_path.get("openid.assoc_handle", [None])[0] + + # With an association handle, look up the appropriate secret key and + # verify the signed items. + + if handle is not None: + + # Where an association exists, use the known secret key. + + if self.associations.has_key(handle): + return check_openid_signature(fields_path, self.associations[handle]) + + # Without an association, request verification of the signed items + # from the OpenID provider. + + else: + return self.test_signature_direct(fields_path) + + # Without a handle, no signature verification can occur. + + return 0 + + def test_signature_direct(self, fields_path): + + """ + See: + http://openid.net/specs/openid-authentication-2_0-12.html#rfc.section.11.4.2 + """ + + # Make a POST request using the "openid." fields. + + d = {} + for name, values in fields_path.items(): + if name.startswith("openid.") and name != "openid.mode": + d[name] = values[0] + d["openid.mode"] = "check_authentication" + data = urllib.urlencode(d) + + # Send a POST request to the OpenID provider, reading the response and + # testing for certain fields and values. + + f = urllib.urlopen(fields_path["openid.op_endpoint"][0], data) + try: + items = [] + for line in f.readlines(): + if line[-1] == "\n": + line = line[:-1] + parts = line.split(":") + items.append((parts[0], ":".join(parts[1:]))) + fields = dict(items) + return fields["ns"] == self.openid_ns and fields["is_valid"] == "true" + finally: + f.close() + + def test_replay(self, fields_path): + + """ + See: + http://openid.net/specs/openid-authentication-2_0-12.html#rfc.section.11.3 + """ + + timestamp = fields_path["openid.response_nonce"][0] + # YYYY-MM-DDTHH:MM:SSZ... + year, month, day, hour, minute, second, unique = \ + int(timestamp[0:4]), int(timestamp[5:7]), int(timestamp[8:10]), \ + int(timestamp[11:13]), int(timestamp[14:16]), int(timestamp[17:19]), \ + timestamp[20:] + dt = datetime.datetime(year, month, day, hour, minute, second) + return -self.replay_limit < (datetime.datetime.utcnow() - dt) < self.replay_limit + + def set_token(self, trans, username): + + "Set an authentication token in 'trans' with the given 'username'." + + Verifier.set_token(self, trans, username) + + # Update the transaction with the user details. + + trans.set_user(username) + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r 7f1f02b485f8 -r 26865b172666 WebStack/Resources/Static.py --- a/WebStack/Resources/Static.py Mon Nov 12 00:50:03 2007 +0000 +++ b/WebStack/Resources/Static.py Mon Nov 12 00:51:34 2007 +0000 @@ -148,4 +148,16 @@ trans.get_response_stream().write(f.read()) f.close() +class StringResource: + + "A resource serving a string as a page." + + def __init__(self, s, content_type): + self.s = s + self.content_type = content_type + + def respond(self, trans): + trans.set_content_type(self.content_type) + trans.get_response_stream().write(self.s) + # vim: tabstop=4 expandtab shiftwidth=4