1.1 --- a/WebStack/Resources/Login.py Mon Nov 12 00:50:03 2007 +0000
1.2 +++ b/WebStack/Resources/Login.py Mon Nov 12 00:51:34 2007 +0000
1.3 @@ -22,7 +22,7 @@
1.4 """
1.5
1.6 import WebStack.Generic
1.7 -from WebStack.Helpers.Auth import get_token
1.8 +from WebStack.Helpers.Auth import LoginAuthenticator
1.9
1.10 class LoginResource:
1.11
1.12 @@ -59,57 +59,59 @@
1.13
1.14 "Respond using the transaction 'trans'."
1.15
1.16 - app, path, qs = get_target(trans, self.urlencoding, self.encoding)
1.17 -
1.18 # Check for a submitted login form.
1.19
1.20 fields_body = trans.get_fields_from_body(self.encoding)
1.21
1.22 if fields_body.has_key("login"):
1.23 if self.authenticator.authenticate(trans, fields_body.get("username", [None])[0], fields_body.get("password", [None])[0]):
1.24 - self._redirect(trans, app, path, qs)
1.25 + app = fields_body.get("app", [""])[0]
1.26 +
1.27 + self._redirect(trans, app)
1.28 # The above method does not return.
1.29
1.30 # Otherwise, show the login form.
1.31
1.32 - self.show_login(trans, app, path, qs)
1.33 + fields_path = trans.get_fields_from_path(self.urlencoding)
1.34 + app = fields_path.get("app", [""])[0]
1.35
1.36 - def _redirect(self, trans, app, path, qs):
1.37 + self.show_login(trans, app)
1.38 +
1.39 + def _redirect(self, trans, app):
1.40
1.41 """
1.42 - Redirect the client using 'trans' and the given 'app', 'path' and 'qs'
1.43 - details.
1.44 + Redirect the client using 'trans' and the given 'app' details.
1.45 """
1.46
1.47 # Show the success page anyway.
1.48
1.49 - self.show_success(trans, app, path, qs)
1.50 + self.show_success(trans, app)
1.51 if self.use_redirect:
1.52 - trans.redirect(app + trans.encode_path(path, self.urlencoding) + qs)
1.53 + trans.redirect(app)
1.54 else:
1.55 raise WebStack.Generic.EndOfResponse
1.56
1.57 - def show_login(self, trans, app, path, qs):
1.58 + def show_login(self, trans, app):
1.59
1.60 """
1.61 Writes a login screen using the transaction 'trans', including details
1.62 - of the 'app', 'path' and 'qs' which the client was attempting to access.
1.63 + of the 'app' which the client was attempting to access.
1.64 """
1.65
1.66 trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding))
1.67 out = trans.get_response_stream()
1.68 - out.write(self.login_page % (app, path, qs))
1.69 + out.write(self.login_page % app)
1.70
1.71 - def show_success(self, trans, app, path, qs):
1.72 + def show_success(self, trans, app):
1.73
1.74 """
1.75 Writes a success screen using the transaction 'trans', including details
1.76 - of the 'app', 'path' and 'qs' which the client was attempting to access.
1.77 + of the 'app' which the client was attempting to access.
1.78 """
1.79
1.80 trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding))
1.81 out = trans.get_response_stream()
1.82 - out.write(self.success_page % (app, trans.encode_path(path, self.urlencoding), qs))
1.83 + out.write(self.success_page % (app, app))
1.84
1.85 login_page = """
1.86 <html>
1.87 @@ -123,8 +125,6 @@
1.88 <p>Password: <input name="password" type="password" size="12"/></p>
1.89 <p><input name="login" type="submit" value="Login"/></p>
1.90 <input name="app" type="hidden" value="%s"/>
1.91 - <input name="path" type="hidden" value="%s"/>
1.92 - <input name="qs" type="hidden" value="%s"/>
1.93 </form>
1.94 </body>
1.95 </html>
1.96 @@ -137,101 +137,9 @@
1.97 </head>
1.98 <body>
1.99 <h1>Login Successful</h1>
1.100 - <p>Please proceed <a href="%s%s%s">to the application</a>.</p>
1.101 + <p>Please proceed to the application: <a href="%s">%s</a></p>
1.102 </body>
1.103 </html>
1.104 """
1.105
1.106 -class LoginAuthenticator:
1.107 -
1.108 - def __init__(self, secret_key, credentials, cookie_name=None):
1.109 -
1.110 - """
1.111 - Initialise the authenticator with a 'secret_key', the authenticator's registry of
1.112 - 'credentials' and an optional 'cookie_name'.
1.113 -
1.114 - The 'credentials' must be an object which supports tests of the form
1.115 - '(username, password) in credentials'.
1.116 - """
1.117 -
1.118 - self.secret_key = secret_key
1.119 - self.credentials = credentials
1.120 - self.cookie_name = cookie_name or "LoginAuthenticator"
1.121 -
1.122 - def authenticate(self, trans, username, password):
1.123 -
1.124 - """
1.125 - Authenticate the sender of the transaction 'trans', returning 1 (true) if they are
1.126 - recognised, 0 (false) otherwise. Use the 'username' and 'password' supplied as
1.127 - credentials.
1.128 - """
1.129 -
1.130 - # Process any supplied parameters.
1.131 -
1.132 - fields = trans.get_fields_from_body()
1.133 -
1.134 - # Check against the class's credentials.
1.135 -
1.136 - if (username, password) in self.credentials:
1.137 -
1.138 - # Make a special cookie token.
1.139 -
1.140 - self.set_token(trans, username)
1.141 - return 1
1.142 -
1.143 - return 0
1.144 -
1.145 - def set_token(self, trans, username):
1.146 -
1.147 - "Set an authentication token in 'trans' with the given 'username'."
1.148 -
1.149 - trans.set_cookie_value(
1.150 - self.cookie_name,
1.151 - get_token(username, self.secret_key),
1.152 - path="/"
1.153 - )
1.154 -
1.155 -# General functions.
1.156 -
1.157 -def get_target(trans, urlencoding=None, encoding=None):
1.158 -
1.159 - """
1.160 - Return the application, path and query string for 'trans' using the optional
1.161 - 'urlencoding' (or path encoding) and request body 'encoding'.
1.162 - """
1.163 -
1.164 - fields_path = trans.get_fields_from_path(urlencoding)
1.165 - fields_body = trans.get_fields_from_body(encoding)
1.166 -
1.167 - # NOTE: Handle missing redirects better.
1.168 -
1.169 - if fields_body.has_key("app"):
1.170 - apps = fields_body["app"]
1.171 - app = apps[0]
1.172 - elif fields_path.has_key("app"):
1.173 - apps = fields_path["app"]
1.174 - app = apps[0]
1.175 - else:
1.176 - app = u""
1.177 -
1.178 - if fields_body.has_key("path"):
1.179 - paths = fields_body["path"]
1.180 - path = paths[0]
1.181 - elif fields_path.has_key("path"):
1.182 - paths = fields_path["path"]
1.183 - path = paths[0]
1.184 - else:
1.185 - path = u""
1.186 -
1.187 - if fields_body.has_key("qs"):
1.188 - qss = fields_body["qs"]
1.189 - qs = qss[0]
1.190 - elif fields_path.has_key("qs"):
1.191 - qss = fields_path["qs"]
1.192 - qs = qss[0]
1.193 - else:
1.194 - qs = u""
1.195 -
1.196 - return app, path, qs
1.197 -
1.198 # vim: tabstop=4 expandtab shiftwidth=4
2.1 --- a/WebStack/Resources/LoginRedirect.py Mon Nov 12 00:50:03 2007 +0000
2.2 +++ b/WebStack/Resources/LoginRedirect.py Mon Nov 12 00:51:34 2007 +0000
2.3 @@ -21,7 +21,7 @@
2.4 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
2.5 """
2.6
2.7 -from WebStack.Helpers.Auth import get_token
2.8 +from WebStack.Helpers.Auth import Verifier
2.9 import WebStack.Generic
2.10
2.11 class LoginRedirectResource:
2.12 @@ -123,16 +123,12 @@
2.13
2.14 # Redirect to the login URL.
2.15
2.16 - path = trans.get_path_without_query(self.path_encoding)
2.17 - qs = trans.get_query_string()
2.18 - if qs:
2.19 - qs = "?" + qs
2.20 - trans.redirect("%s?app=%s&path=%s&qs=%s" % (
2.21 + path = trans.get_path(self.path_encoding)
2.22 + trans.redirect("%s?app=%s%s" % (
2.23 self.get_login_url(trans),
2.24 trans.encode_path(self.get_app_url(trans), self.path_encoding),
2.25 trans.encode_path(path, self.path_encoding),
2.26 - trans.encode_path(qs, self.path_encoding))
2.27 - )
2.28 + ))
2.29
2.30 def get_app_url(self, trans):
2.31
2.32 @@ -182,61 +178,38 @@
2.33 </html>
2.34 """
2.35
2.36 -class LoginRedirectAuthenticator:
2.37 +class LoginRedirectAuthenticator(Verifier):
2.38
2.39 """
2.40 - An authenticator which verifies the credentials provided in a special login cookie.
2.41 + An authenticator which verifies the credentials provided in a special login
2.42 + cookie.
2.43 """
2.44
2.45 - def __init__(self, secret_key, cookie_name=None):
2.46 -
2.47 - "Initialise the authenticator with a 'secret_key' and an optional 'cookie_name'."
2.48 -
2.49 - self.secret_key = secret_key
2.50 - self.cookie_name = cookie_name or "LoginAuthenticator"
2.51 -
2.52 def authenticate(self, trans):
2.53
2.54 """
2.55 - Authenticate the originator of 'trans', updating the object if successful and
2.56 - returning 1 (true) if successful, 0 (false) otherwise.
2.57 + Authenticate the originator of 'trans', updating the object if
2.58 + successful and returning a true value if successful, or a false value
2.59 + otherwise.
2.60 """
2.61
2.62 - cookie = trans.get_cookie(self.cookie_name)
2.63 - if cookie is None or cookie.value is None:
2.64 - return 0
2.65 + valid = Verifier.authenticate(self, trans)
2.66
2.67 - # Test the token from the cookie against a recreated token using the
2.68 - # given information.
2.69 + # Update the transaction with the user details.
2.70
2.71 - username = cookie.value.split(":")[0]
2.72 - if cookie.value == get_token(username, self.secret_key):
2.73 -
2.74 - # Update the transaction with the user details.
2.75 -
2.76 + if valid:
2.77 + username, token = self.get_username_and_token(trans)
2.78 trans.set_user(username)
2.79 - return 1
2.80 - else:
2.81 - return 0
2.82 + return valid
2.83
2.84 def set_token(self, trans, username):
2.85
2.86 "Set an authentication token in 'trans' with the given 'username'."
2.87
2.88 - trans.set_cookie_value(
2.89 - self.cookie_name,
2.90 - get_token(username, self.secret_key),
2.91 - path="/"
2.92 - )
2.93 + Verifier.set_token(self, trans, username)
2.94
2.95 # Update the transaction with the user details.
2.96
2.97 trans.set_user(username)
2.98
2.99 - def unset_token(self, trans):
2.100 -
2.101 - "Unset the authentication token in 'trans'."
2.102 -
2.103 - trans.delete_cookie(self.cookie_name)
2.104 -
2.105 # vim: tabstop=4 expandtab shiftwidth=4
3.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
3.2 +++ b/WebStack/Resources/OpenIDInitiation.py Mon Nov 12 00:51:34 2007 +0000
3.3 @@ -0,0 +1,223 @@
3.4 +#!/usr/bin/env python
3.5 +
3.6 +"""
3.7 +OpenID initiation resources which redirect clients to an OpenID provider.
3.8 +
3.9 +Copyright (C) 2004, 2005, 2006, 2007 Paul Boddie <paul@boddie.org.uk>
3.10 +
3.11 +This library is free software; you can redistribute it and/or
3.12 +modify it under the terms of the GNU Lesser General Public
3.13 +License as published by the Free Software Foundation; either
3.14 +version 2.1 of the License, or (at your option) any later version.
3.15 +
3.16 +This library is distributed in the hope that it will be useful,
3.17 +but WITHOUT ANY WARRANTY; without even the implied warranty of
3.18 +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
3.19 +Lesser General Public License for more details.
3.20 +
3.21 +You should have received a copy of the GNU Lesser General Public
3.22 +License along with this library; if not, write to the Free Software
3.23 +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
3.24 +"""
3.25 +
3.26 +import WebStack.Generic
3.27 +import libxml2dom
3.28 +
3.29 +class OpenIDInitiationResource:
3.30 +
3.31 + "A resource providing an OpenID initiation screen."
3.32 +
3.33 + encoding = "utf-8"
3.34 + openid_ns = "http://specs.openid.net/auth/2.0"
3.35 +
3.36 + def __init__(self, openid_mode=None, use_redirect=1, urlencoding=None, encoding=None):
3.37 +
3.38 + """
3.39 + Initialise the resource.
3.40 +
3.41 + The optional 'openid_mode' parameter may be set to "checkid_immediate"
3.42 + or "checkid_setup" (the default).
3.43 +
3.44 + If the optional 'use_redirect' flag is set to 0, a confirmation screen
3.45 + is given instead of redirecting the user back to the original
3.46 + application.
3.47 +
3.48 + The optional 'urlencoding' parameter allows a special encoding to be
3.49 + used in producing the redirection path.
3.50 +
3.51 + The optional 'encoding' parameter allows a special encoding to be used
3.52 + in producing the initiation pages.
3.53 +
3.54 + To change the pages employed by this resource, either redefine the
3.55 + 'initiation_page' and 'success_page' attributes in instances of this class or
3.56 + a subclass, or override the 'show_initiation' and 'show_success' methods.
3.57 + """
3.58 +
3.59 + self.openid_mode = openid_mode or "checkid_setup"
3.60 + self.use_redirect = use_redirect
3.61 + self.urlencoding = urlencoding
3.62 + self.encoding = encoding or self.encoding
3.63 +
3.64 + def respond(self, trans):
3.65 +
3.66 + "Respond using the transaction 'trans'."
3.67 +
3.68 + app = get_target(trans, self.urlencoding, self.encoding)
3.69 +
3.70 + # Check for a submitted initiation form.
3.71 +
3.72 + fields_body = trans.get_fields_from_body(self.encoding)
3.73 +
3.74 + if fields_body.has_key("initiate") and fields_body.has_key("identity"):
3.75 + claimed_identifier, provider, local_identifier = self.get_provider_url(fields_body["identity"][0])
3.76 + if provider is not None:
3.77 + self._redirect(trans, app, claimed_identifier, provider, local_identifier)
3.78 + # The above method does not return.
3.79 +
3.80 + # Otherwise, show the initiation form.
3.81 +
3.82 + self.show_initiation(trans, app)
3.83 +
3.84 + def _redirect(self, trans, app, claimed_identifier, provider, local_identifier):
3.85 +
3.86 + """
3.87 + Redirect the client using 'trans' and the given 'app',
3.88 + 'claimed_identifier', 'provider' and 'local_identifier' details.
3.89 +
3.90 + See:
3.91 + http://openid.net/specs/openid-authentication-2_0-12.html#rfc.section.5.2
3.92 + http://openid.net/specs/openid-authentication-2_0-12.html#rfc.section.9
3.93 + """
3.94 +
3.95 + # NOTE: Should consider the special "select" mode for identity.
3.96 +
3.97 + url = "%s?openid.ns=%s&openid.mode=%s&openid.return_to=%s&openid.claimed_id=%s&openid.identity=%s" % (
3.98 + provider,
3.99 + trans.encode_path(self.openid_ns, self.urlencoding),
3.100 + trans.encode_path(self.openid_mode, self.urlencoding),
3.101 + trans.encode_path(app, self.urlencoding),
3.102 + trans.encode_path(claimed_identifier, self.urlencoding),
3.103 + trans.encode_path(local_identifier, self.urlencoding)
3.104 + )
3.105 +
3.106 + # Show the success page anyway.
3.107 +
3.108 + self.show_success(trans, url)
3.109 +
3.110 + # Redirect to the OpenID provider URL.
3.111 +
3.112 + if self.use_redirect:
3.113 + trans.redirect(url)
3.114 + else:
3.115 + raise WebStack.Generic.EndOfResponse
3.116 +
3.117 + def get_provider_url(self, identity):
3.118 +
3.119 + """
3.120 + Return the claimed identifier, provider URL and local identifier for the
3.121 + authenticating user using the given 'identity'.
3.122 +
3.123 + See:
3.124 + http://openid.net/specs/openid-authentication-2_0-12.html#rfc.section.7.3
3.125 + """
3.126 +
3.127 + if identity.startswith("xri://"):
3.128 + identity = openid[6:]
3.129 +
3.130 + # NOTE: Not yet discovering XRI providers.
3.131 +
3.132 + if identity[0] in ("=", "@", "+", "$", "!", "("):
3.133 + pass
3.134 + else:
3.135 + if not identity.startswith("http"):
3.136 + identity = "http://" + identity
3.137 +
3.138 + # Obtain a provider url from a resource at the stated URL.
3.139 +
3.140 + doc = libxml2dom.parseURI(identity, html=1)
3.141 + provider_links = doc.xpath("/html/head/link[contains(@rel, 'openid2.provider')]/@href")
3.142 + local_ids = doc.xpath("/html/head/link[contains(@rel, 'openid2.local_id')]/@href")
3.143 + if provider_links:
3.144 + if local_ids:
3.145 + return identity, provider_links[0].nodeValue, local_ids[0].nodeValue
3.146 + else:
3.147 + return identity, provider_links[0].nodeValue, None
3.148 +
3.149 + return identity, None, None
3.150 +
3.151 + def show_initiation(self, trans, app):
3.152 +
3.153 + """
3.154 + Writes a initiation screen using the transaction 'trans', including details
3.155 + of the 'app' which the client was attempting to access.
3.156 + """
3.157 +
3.158 + trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding))
3.159 + out = trans.get_response_stream()
3.160 + out.write(self.initiation_page % app)
3.161 +
3.162 + def show_success(self, trans, url):
3.163 +
3.164 + """
3.165 + Writes a success screen using the transaction 'trans', including details
3.166 + of the OpenID provider 'url'.
3.167 + """
3.168 +
3.169 + trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding))
3.170 + out = trans.get_response_stream()
3.171 + out.write(self.success_page % (url, url))
3.172 +
3.173 + initiation_page = """
3.174 +<html>
3.175 + <head>
3.176 + <title>Authenticate via OpenID</title>
3.177 + </head>
3.178 + <body>
3.179 + <h1>Authenticate via OpenID</h1>
3.180 + <form method="POST" name="openid_identifier">
3.181 + <p>OpenID Identifier (URL): <input name="identity" type="text" size="32"/></p>
3.182 + <p><input name="initiate" type="submit" value="Login"/></p>
3.183 + <input name="app" type="hidden" value="%s"/>
3.184 + </form>
3.185 + </body>
3.186 +</html>
3.187 +"""
3.188 +
3.189 + success_page = """
3.190 +<html>
3.191 + <head>
3.192 + <title>Authenticate via OpenID</title>
3.193 + </head>
3.194 + <body>
3.195 + <h1>Authenticate via OpenID</h1>
3.196 + <p>Please proceed to the OpenID provider: <a href="%s">%s</a>.</p>
3.197 + </body>
3.198 +</html>
3.199 +"""
3.200 +
3.201 +# General functions.
3.202 +
3.203 +def get_target(trans, urlencoding=None, encoding=None):
3.204 +
3.205 + """
3.206 + Return the application for 'trans' using the optional 'urlencoding' (or path
3.207 + encoding) and request body 'encoding'.
3.208 + """
3.209 +
3.210 + fields_path = trans.get_fields_from_path(urlencoding)
3.211 + fields_body = trans.get_fields_from_body(encoding)
3.212 +
3.213 + # NOTE: Handle missing redirects better.
3.214 +
3.215 + if fields_body.has_key("app"):
3.216 + apps = fields_body["app"]
3.217 + app = apps[0]
3.218 + elif fields_path.has_key("app"):
3.219 + apps = fields_path["app"]
3.220 + app = apps[0]
3.221 + else:
3.222 + app = u""
3.223 +
3.224 + return app
3.225 +
3.226 +# vim: tabstop=4 expandtab shiftwidth=4
4.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
4.2 +++ b/WebStack/Resources/OpenIDLogin.py Mon Nov 12 00:51:34 2007 +0000
4.3 @@ -0,0 +1,247 @@
4.4 +#!/usr/bin/env python
4.5 +
4.6 +"""
4.7 +OpenID provider login resources which redirect clients back to the application
4.8 +("relying party").
4.9 +
4.10 +Copyright (C) 2004, 2005, 2006, 2007 Paul Boddie <paul@boddie.org.uk>
4.11 +
4.12 +This library is free software; you can redistribute it and/or
4.13 +modify it under the terms of the GNU Lesser General Public
4.14 +License as published by the Free Software Foundation; either
4.15 +version 2.1 of the License, or (at your option) any later version.
4.16 +
4.17 +This library is distributed in the hope that it will be useful,
4.18 +but WITHOUT ANY WARRANTY; without even the implied warranty of
4.19 +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
4.20 +Lesser General Public License for more details.
4.21 +
4.22 +You should have received a copy of the GNU Lesser General Public
4.23 +License along with this library; if not, write to the Free Software
4.24 +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
4.25 +"""
4.26 +
4.27 +import WebStack.Generic
4.28 +from WebStack.Helpers.Auth import Authenticator, check_openid_signature, make_openid_signature
4.29 +import datetime
4.30 +import time
4.31 +import random
4.32 +
4.33 +class OpenIDLoginResource:
4.34 +
4.35 + "A resource providing a login screen."
4.36 +
4.37 + encoding = "utf-8"
4.38 + openid_ns = "http://specs.openid.net/auth/2.0"
4.39 +
4.40 + def __init__(self, app_url, authenticator, associations=None, use_redirect=1, urlencoding=None, encoding=None):
4.41 +
4.42 + """
4.43 + Initialise the resource with the application URL 'app_url' and an
4.44 + 'authenticator'.
4.45 +
4.46 + The optional 'associations' is a mapping from association handles to
4.47 + secret keys.
4.48 +
4.49 + If the optional 'use_redirect' flag is set to 0, a confirmation screen
4.50 + is given instead of redirecting the user back to the original
4.51 + application.
4.52 +
4.53 + The optional 'urlencoding' parameter allows a special encoding to be
4.54 + used in producing the redirection path.
4.55 +
4.56 + The optional 'encoding' parameter allows a special encoding to be used
4.57 + in producing the login pages.
4.58 +
4.59 + To change the pages employed by this resource, either redefine the
4.60 + 'login_page' and 'success_page' attributes in instances of this class or
4.61 + a subclass, or override the 'show_login' and 'show_success' methods.
4.62 + """
4.63 +
4.64 + self.app_url = app_url
4.65 + self.authenticator = authenticator
4.66 + self.associations = associations or {}
4.67 + self.use_redirect = use_redirect
4.68 + self.urlencoding = urlencoding
4.69 + self.encoding = encoding or self.encoding
4.70 +
4.71 + def respond(self, trans):
4.72 +
4.73 + "Respond using the transaction 'trans'."
4.74 +
4.75 + # Check for a submitted login form.
4.76 +
4.77 + fields_body = trans.get_fields_from_body(self.encoding)
4.78 +
4.79 + if fields_body.has_key("login"):
4.80 +
4.81 + # Check a combination of local identifier and username together with
4.82 + # the password.
4.83 +
4.84 + claimed_id = fields_body.get("claimed_id", [""])[0]
4.85 + local_id = fields_body.get("local_id", [""])[0]
4.86 + username = fields_body.get("username", [""])[0]
4.87 + password = fields_body.get("password", [""])[0]
4.88 + app = fields_body.get("app", [""])[0]
4.89 +
4.90 + if self.authenticator.authenticate(trans, (local_id, username), password):
4.91 + self._redirect(trans, claimed_id, local_id, username, app)
4.92 + # The above method does not return.
4.93 +
4.94 + # Check for an OpenID signature verification request.
4.95 +
4.96 + elif fields_body.get("openid.mode", [None])[0] == "check_authentication":
4.97 +
4.98 + # Obtain the secret key from recorded associations.
4.99 +
4.100 + handle = fields_body.get("openid.assoc_handle", [None])[0]
4.101 + if handle is not None and self.associations.has_key(handle):
4.102 + valid = check_openid_signature(fields_body, self.associations[handle])
4.103 + del self.associations[handle]
4.104 + else:
4.105 + valid = 0
4.106 +
4.107 + # Produce a response for this request.
4.108 +
4.109 + self.show_verification(trans, valid)
4.110 + # The above method does not return.
4.111 +
4.112 + # Otherwise, show the login form.
4.113 +
4.114 + fields_path = trans.get_fields_from_path(self.urlencoding)
4.115 + app = fields_path.get("openid.return_to", [""])[0]
4.116 + claimed_id = fields_path.get("openid.claimed_id", [""])[0]
4.117 + local_id = fields_path.get("openid.identity", [""])[0]
4.118 +
4.119 + self.show_login(trans, app, claimed_id, local_id)
4.120 +
4.121 + def _redirect(self, trans, claimed_id, local_id, username, app):
4.122 +
4.123 + """
4.124 + Redirect the client using 'trans', 'claimed_id', 'local_id', 'username'
4.125 + and the given 'app' details.
4.126 + """
4.127 +
4.128 + app_url = self.app_url + trans.get_path_without_query(self.urlencoding)
4.129 +
4.130 + # Make an association that can be used in signature verification.
4.131 + # NOTE: Probably need to consider the secret key a bit more.
4.132 +
4.133 + handle = username + str(time.time())
4.134 + secret_key = str(random.randint(0, 999999999))
4.135 + self.associations[handle] = secret_key
4.136 +
4.137 + # Make a timestamp.
4.138 +
4.139 + now = datetime.datetime.utcnow()
4.140 + timestamp = now.strftime("%Y-%m-%dT%H:%M:%SZ") + str(now.microsecond)
4.141 +
4.142 + # Make a signature.
4.143 +
4.144 + signed_names = ["op_endpoint", "return_to", "response_nonce", "assoc_handle", "claimed_id", "identity"]
4.145 + fields = {
4.146 + "openid.op_endpoint" : [app_url],
4.147 + "openid.return_to" : [app],
4.148 + "openid.response_nonce" : [timestamp],
4.149 + "openid.assoc_handle" : [handle],
4.150 + "openid.claimed_id" : [claimed_id],
4.151 + "openid.identity" : [local_id]
4.152 + }
4.153 +
4.154 + signature = make_openid_signature(signed_names, fields, secret_key)
4.155 +
4.156 + # Build an URL for returning to the application.
4.157 +
4.158 + url = "%s?openid.ns=%s&openid.mode=%s&openid.signed=%s&openid.sig=%s" % (
4.159 + app,
4.160 + trans.encode_path(self.openid_ns, self.urlencoding),
4.161 + trans.encode_path("id_res", self.urlencoding),
4.162 + trans.encode_path(",".join(signed_names), self.urlencoding),
4.163 + trans.encode_path(signature, self.urlencoding)
4.164 + )
4.165 +
4.166 + for name, value in fields.items():
4.167 + url += "&%s=%s" % (name, trans.encode_path(value[0], self.urlencoding))
4.168 +
4.169 + # Show the success page anyway.
4.170 +
4.171 + self.show_success(trans, url)
4.172 + if self.use_redirect:
4.173 + trans.redirect(url)
4.174 + else:
4.175 + raise WebStack.Generic.EndOfResponse
4.176 +
4.177 + def show_verification(self, trans, status):
4.178 +
4.179 + """
4.180 + Writes a signature verification response using the transaction 'trans'
4.181 + and the 'status' of the verification.
4.182 + """
4.183 +
4.184 + trans.set_content_type(WebStack.Generic.ContentType("text/plain"))
4.185 + out = trans.get_response_stream()
4.186 +
4.187 + # NOTE: Need to use invalidate_handle, too.
4.188 +
4.189 + if status:
4.190 + status_str = "true"
4.191 + else:
4.192 + status_str = "false"
4.193 + out.write("ns:%s\nis_valid:%s\n" % (self.openid_ns, status_str))
4.194 + raise WebStack.Generic.EndOfResponse
4.195 +
4.196 + def show_login(self, trans, app, claimed_id, local_id):
4.197 +
4.198 + """
4.199 + Writes a login screen using the transaction 'trans', including details
4.200 + of the 'app' which the client was attempting to access, along with the
4.201 + 'claimed_id' and 'local_id'.
4.202 + """
4.203 +
4.204 + trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding))
4.205 + out = trans.get_response_stream()
4.206 + out.write(self.login_page % (app, claimed_id, local_id))
4.207 +
4.208 + def show_success(self, trans, app):
4.209 +
4.210 + """
4.211 + Writes a success screen using the transaction 'trans', including details
4.212 + of the 'app' which the client was attempting to access.
4.213 + """
4.214 +
4.215 + trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding))
4.216 + out = trans.get_response_stream()
4.217 + out.write(self.success_page % (app, app))
4.218 +
4.219 + login_page = """
4.220 +<html>
4.221 + <head>
4.222 + <title>Login</title>
4.223 + </head>
4.224 + <body>
4.225 + <h1>Login</h1>
4.226 + <form method="POST">
4.227 + <p>Username: <input name="username" type="text" size="12"/></p>
4.228 + <p>Password: <input name="password" type="password" size="12"/></p>
4.229 + <p><input name="login" type="submit" value="Login"/></p>
4.230 + <input name="app" type="hidden" value="%s"/>
4.231 + <input name="claimed_id" type="hidden" value="%s"/>
4.232 + <input name="local_id" type="hidden" value="%s"/>
4.233 + </form>
4.234 + </body>
4.235 +</html>
4.236 +"""
4.237 +
4.238 + success_page = """
4.239 +<html>
4.240 + <head>
4.241 + <title>Login Example</title>
4.242 + </head>
4.243 + <body>
4.244 + <h1>Login Successful</h1>
4.245 + <p>Please proceed to the application: <a href="%s">%s</a></p>
4.246 + </body>
4.247 +</html>
4.248 +"""
4.249 +
4.250 +# vim: tabstop=4 expandtab shiftwidth=4
5.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
5.2 +++ b/WebStack/Resources/OpenIDRedirect.py Mon Nov 12 00:51:34 2007 +0000
5.3 @@ -0,0 +1,205 @@
5.4 +#!/usr/bin/env python
5.5 +
5.6 +"""
5.7 +OpenID redirection classes, sending unauthenticated users to the OpenID
5.8 +initiation page.
5.9 +
5.10 +Copyright (C) 2004, 2005, 2006, 2007 Paul Boddie <paul@boddie.org.uk>
5.11 +
5.12 +This library is free software; you can redistribute it and/or
5.13 +modify it under the terms of the GNU Lesser General Public
5.14 +License as published by the Free Software Foundation; either
5.15 +version 2.1 of the License, or (at your option) any later version.
5.16 +
5.17 +This library is distributed in the hope that it will be useful,
5.18 +but WITHOUT ANY WARRANTY; without even the implied warranty of
5.19 +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
5.20 +Lesser General Public License for more details.
5.21 +
5.22 +You should have received a copy of the GNU Lesser General Public
5.23 +License along with this library; if not, write to the Free Software
5.24 +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
5.25 +"""
5.26 +
5.27 +from WebStack.Helpers.Auth import Verifier, check_openid_signature
5.28 +import WebStack.Generic
5.29 +import datetime
5.30 +import urllib
5.31 +
5.32 +class OpenIDRedirectAuthenticator(Verifier):
5.33 +
5.34 + """
5.35 + An authenticator which verifies the credentials provided in a special login
5.36 + cookie, accepting OpenID assertions if necessary.
5.37 + """
5.38 +
5.39 + encoding = "utf-8"
5.40 + openid_ns = "http://specs.openid.net/auth/2.0"
5.41 + replay_limit = datetime.timedelta(0, 10) # 10s
5.42 +
5.43 + def __init__(self, secret_key, app_url, associations=None, replay_limit=None,
5.44 + cookie_name=None, urlencoding=None):
5.45 +
5.46 + """
5.47 + Initialise the authenticator with a 'secret_key', 'app_url' and optional
5.48 + 'associations', 'replay_limit', 'cookie_name' and 'urlencoding'.
5.49 + """
5.50 +
5.51 + Verifier.__init__(self, secret_key, cookie_name)
5.52 +
5.53 + self.app_url = app_url
5.54 + self.associations = associations or {}
5.55 + self.replay_limit = replay_limit or self.replay_limit
5.56 + self.urlencoding = urlencoding or self.encoding
5.57 +
5.58 + def authenticate(self, trans):
5.59 +
5.60 + """
5.61 + Authenticate the originator of 'trans', updating the object if
5.62 + successful and returning a true value if successful, or a false value
5.63 + otherwise.
5.64 + """
5.65 +
5.66 + # First, try to authenticate with an application cookie.
5.67 +
5.68 + valid = Verifier.authenticate(self, trans)
5.69 +
5.70 + # Without a valid login, attempt to verify OpenID assertions.
5.71 + # http://openid.net/specs/openid-authentication-2_0-12.html#rfc.section.11
5.72 +
5.73 + if not valid:
5.74 + fields_path = trans.get_fields_from_path(self.urlencoding)
5.75 +
5.76 + if fields_path.get("openid.ns", [None])[0] == self.openid_ns and \
5.77 + fields_path.get("openid.mode", [None])[0] == "id_res":
5.78 +
5.79 + # NOTE: Could expose all errors.
5.80 +
5.81 + try:
5.82 +
5.83 + if self.test_url(fields_path) and \
5.84 + self.test_signature(fields_path) and \
5.85 + self.test_replay(fields_path):
5.86 +
5.87 + self.set_token(trans, fields_path["openid.identity"][0])
5.88 +
5.89 + # NOTE: Should return true and let the redirector do this.
5.90 + trans.redirect(fields_path["openid.return_to"][0])
5.91 + #return 1
5.92 +
5.93 + # Incomplete assertion.
5.94 +
5.95 + except (KeyError, ValueError):
5.96 + raise
5.97 +
5.98 + # Assertion failed or was incomplete.
5.99 +
5.100 + return 0
5.101 +
5.102 + # Update the transaction with the user details.
5.103 +
5.104 + if valid:
5.105 + username, token = self.get_username_and_token(trans)
5.106 + trans.set_user(username)
5.107 + return valid
5.108 +
5.109 + def test_url(self, fields_path):
5.110 +
5.111 + """
5.112 + See:
5.113 + http://openid.net/specs/openid-authentication-2_0-12.html#rfc.section.11.1
5.114 + """
5.115 +
5.116 + # NOTE: Currently, this is not strict enough.
5.117 +
5.118 + return fields_path["openid.return_to"][0].startswith(self.app_url)
5.119 +
5.120 + def test_signature(self, fields_path):
5.121 +
5.122 + """
5.123 + See:
5.124 + http://openid.net/specs/openid-authentication-2_0-12.html#rfc.section.11.4
5.125 + http://openid.net/specs/openid-authentication-2_0-12.html#rfc.section.6
5.126 + """
5.127 +
5.128 + handle = fields_path.get("openid.assoc_handle", [None])[0]
5.129 +
5.130 + # With an association handle, look up the appropriate secret key and
5.131 + # verify the signed items.
5.132 +
5.133 + if handle is not None:
5.134 +
5.135 + # Where an association exists, use the known secret key.
5.136 +
5.137 + if self.associations.has_key(handle):
5.138 + return check_openid_signature(fields_path, self.associations[handle])
5.139 +
5.140 + # Without an association, request verification of the signed items
5.141 + # from the OpenID provider.
5.142 +
5.143 + else:
5.144 + return self.test_signature_direct(fields_path)
5.145 +
5.146 + # Without a handle, no signature verification can occur.
5.147 +
5.148 + return 0
5.149 +
5.150 + def test_signature_direct(self, fields_path):
5.151 +
5.152 + """
5.153 + See:
5.154 + http://openid.net/specs/openid-authentication-2_0-12.html#rfc.section.11.4.2
5.155 + """
5.156 +
5.157 + # Make a POST request using the "openid." fields.
5.158 +
5.159 + d = {}
5.160 + for name, values in fields_path.items():
5.161 + if name.startswith("openid.") and name != "openid.mode":
5.162 + d[name] = values[0]
5.163 + d["openid.mode"] = "check_authentication"
5.164 + data = urllib.urlencode(d)
5.165 +
5.166 + # Send a POST request to the OpenID provider, reading the response and
5.167 + # testing for certain fields and values.
5.168 +
5.169 + f = urllib.urlopen(fields_path["openid.op_endpoint"][0], data)
5.170 + try:
5.171 + items = []
5.172 + for line in f.readlines():
5.173 + if line[-1] == "\n":
5.174 + line = line[:-1]
5.175 + parts = line.split(":")
5.176 + items.append((parts[0], ":".join(parts[1:])))
5.177 + fields = dict(items)
5.178 + return fields["ns"] == self.openid_ns and fields["is_valid"] == "true"
5.179 + finally:
5.180 + f.close()
5.181 +
5.182 + def test_replay(self, fields_path):
5.183 +
5.184 + """
5.185 + See:
5.186 + http://openid.net/specs/openid-authentication-2_0-12.html#rfc.section.11.3
5.187 + """
5.188 +
5.189 + timestamp = fields_path["openid.response_nonce"][0]
5.190 + # YYYY-MM-DDTHH:MM:SSZ...
5.191 + year, month, day, hour, minute, second, unique = \
5.192 + int(timestamp[0:4]), int(timestamp[5:7]), int(timestamp[8:10]), \
5.193 + int(timestamp[11:13]), int(timestamp[14:16]), int(timestamp[17:19]), \
5.194 + timestamp[20:]
5.195 + dt = datetime.datetime(year, month, day, hour, minute, second)
5.196 + return -self.replay_limit < (datetime.datetime.utcnow() - dt) < self.replay_limit
5.197 +
5.198 + def set_token(self, trans, username):
5.199 +
5.200 + "Set an authentication token in 'trans' with the given 'username'."
5.201 +
5.202 + Verifier.set_token(self, trans, username)
5.203 +
5.204 + # Update the transaction with the user details.
5.205 +
5.206 + trans.set_user(username)
5.207 +
5.208 +# vim: tabstop=4 expandtab shiftwidth=4
6.1 --- a/WebStack/Resources/Static.py Mon Nov 12 00:50:03 2007 +0000
6.2 +++ b/WebStack/Resources/Static.py Mon Nov 12 00:51:34 2007 +0000
6.3 @@ -148,4 +148,16 @@
6.4 trans.get_response_stream().write(f.read())
6.5 f.close()
6.6
6.7 +class StringResource:
6.8 +
6.9 + "A resource serving a string as a page."
6.10 +
6.11 + def __init__(self, s, content_type):
6.12 + self.s = s
6.13 + self.content_type = content_type
6.14 +
6.15 + def respond(self, trans):
6.16 + trans.set_content_type(self.content_type)
6.17 + trans.get_response_stream().write(self.s)
6.18 +
6.19 # vim: tabstop=4 expandtab shiftwidth=4