1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
1.2 +++ b/WebStack/Resources/OpenIDLogin.py Mon Nov 12 00:51:34 2007 +0000
1.3 @@ -0,0 +1,247 @@
1.4 +#!/usr/bin/env python
1.5 +
1.6 +"""
1.7 +OpenID provider login resources which redirect clients back to the application
1.8 +("relying party").
1.9 +
1.10 +Copyright (C) 2004, 2005, 2006, 2007 Paul Boddie <paul@boddie.org.uk>
1.11 +
1.12 +This library is free software; you can redistribute it and/or
1.13 +modify it under the terms of the GNU Lesser General Public
1.14 +License as published by the Free Software Foundation; either
1.15 +version 2.1 of the License, or (at your option) any later version.
1.16 +
1.17 +This library is distributed in the hope that it will be useful,
1.18 +but WITHOUT ANY WARRANTY; without even the implied warranty of
1.19 +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1.20 +Lesser General Public License for more details.
1.21 +
1.22 +You should have received a copy of the GNU Lesser General Public
1.23 +License along with this library; if not, write to the Free Software
1.24 +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
1.25 +"""
1.26 +
1.27 +import WebStack.Generic
1.28 +from WebStack.Helpers.Auth import Authenticator, check_openid_signature, make_openid_signature
1.29 +import datetime
1.30 +import time
1.31 +import random
1.32 +
1.33 +class OpenIDLoginResource:
1.34 +
1.35 + "A resource providing a login screen."
1.36 +
1.37 + encoding = "utf-8"
1.38 + openid_ns = "http://specs.openid.net/auth/2.0"
1.39 +
1.40 + def __init__(self, app_url, authenticator, associations=None, use_redirect=1, urlencoding=None, encoding=None):
1.41 +
1.42 + """
1.43 + Initialise the resource with the application URL 'app_url' and an
1.44 + 'authenticator'.
1.45 +
1.46 + The optional 'associations' is a mapping from association handles to
1.47 + secret keys.
1.48 +
1.49 + If the optional 'use_redirect' flag is set to 0, a confirmation screen
1.50 + is given instead of redirecting the user back to the original
1.51 + application.
1.52 +
1.53 + The optional 'urlencoding' parameter allows a special encoding to be
1.54 + used in producing the redirection path.
1.55 +
1.56 + The optional 'encoding' parameter allows a special encoding to be used
1.57 + in producing the login pages.
1.58 +
1.59 + To change the pages employed by this resource, either redefine the
1.60 + 'login_page' and 'success_page' attributes in instances of this class or
1.61 + a subclass, or override the 'show_login' and 'show_success' methods.
1.62 + """
1.63 +
1.64 + self.app_url = app_url
1.65 + self.authenticator = authenticator
1.66 + self.associations = associations or {}
1.67 + self.use_redirect = use_redirect
1.68 + self.urlencoding = urlencoding
1.69 + self.encoding = encoding or self.encoding
1.70 +
1.71 + def respond(self, trans):
1.72 +
1.73 + "Respond using the transaction 'trans'."
1.74 +
1.75 + # Check for a submitted login form.
1.76 +
1.77 + fields_body = trans.get_fields_from_body(self.encoding)
1.78 +
1.79 + if fields_body.has_key("login"):
1.80 +
1.81 + # Check a combination of local identifier and username together with
1.82 + # the password.
1.83 +
1.84 + claimed_id = fields_body.get("claimed_id", [""])[0]
1.85 + local_id = fields_body.get("local_id", [""])[0]
1.86 + username = fields_body.get("username", [""])[0]
1.87 + password = fields_body.get("password", [""])[0]
1.88 + app = fields_body.get("app", [""])[0]
1.89 +
1.90 + if self.authenticator.authenticate(trans, (local_id, username), password):
1.91 + self._redirect(trans, claimed_id, local_id, username, app)
1.92 + # The above method does not return.
1.93 +
1.94 + # Check for an OpenID signature verification request.
1.95 +
1.96 + elif fields_body.get("openid.mode", [None])[0] == "check_authentication":
1.97 +
1.98 + # Obtain the secret key from recorded associations.
1.99 +
1.100 + handle = fields_body.get("openid.assoc_handle", [None])[0]
1.101 + if handle is not None and self.associations.has_key(handle):
1.102 + valid = check_openid_signature(fields_body, self.associations[handle])
1.103 + del self.associations[handle]
1.104 + else:
1.105 + valid = 0
1.106 +
1.107 + # Produce a response for this request.
1.108 +
1.109 + self.show_verification(trans, valid)
1.110 + # The above method does not return.
1.111 +
1.112 + # Otherwise, show the login form.
1.113 +
1.114 + fields_path = trans.get_fields_from_path(self.urlencoding)
1.115 + app = fields_path.get("openid.return_to", [""])[0]
1.116 + claimed_id = fields_path.get("openid.claimed_id", [""])[0]
1.117 + local_id = fields_path.get("openid.identity", [""])[0]
1.118 +
1.119 + self.show_login(trans, app, claimed_id, local_id)
1.120 +
1.121 + def _redirect(self, trans, claimed_id, local_id, username, app):
1.122 +
1.123 + """
1.124 + Redirect the client using 'trans', 'claimed_id', 'local_id', 'username'
1.125 + and the given 'app' details.
1.126 + """
1.127 +
1.128 + app_url = self.app_url + trans.get_path_without_query(self.urlencoding)
1.129 +
1.130 + # Make an association that can be used in signature verification.
1.131 + # NOTE: Probably need to consider the secret key a bit more.
1.132 +
1.133 + handle = username + str(time.time())
1.134 + secret_key = str(random.randint(0, 999999999))
1.135 + self.associations[handle] = secret_key
1.136 +
1.137 + # Make a timestamp.
1.138 +
1.139 + now = datetime.datetime.utcnow()
1.140 + timestamp = now.strftime("%Y-%m-%dT%H:%M:%SZ") + str(now.microsecond)
1.141 +
1.142 + # Make a signature.
1.143 +
1.144 + signed_names = ["op_endpoint", "return_to", "response_nonce", "assoc_handle", "claimed_id", "identity"]
1.145 + fields = {
1.146 + "openid.op_endpoint" : [app_url],
1.147 + "openid.return_to" : [app],
1.148 + "openid.response_nonce" : [timestamp],
1.149 + "openid.assoc_handle" : [handle],
1.150 + "openid.claimed_id" : [claimed_id],
1.151 + "openid.identity" : [local_id]
1.152 + }
1.153 +
1.154 + signature = make_openid_signature(signed_names, fields, secret_key)
1.155 +
1.156 + # Build an URL for returning to the application.
1.157 +
1.158 + url = "%s?openid.ns=%s&openid.mode=%s&openid.signed=%s&openid.sig=%s" % (
1.159 + app,
1.160 + trans.encode_path(self.openid_ns, self.urlencoding),
1.161 + trans.encode_path("id_res", self.urlencoding),
1.162 + trans.encode_path(",".join(signed_names), self.urlencoding),
1.163 + trans.encode_path(signature, self.urlencoding)
1.164 + )
1.165 +
1.166 + for name, value in fields.items():
1.167 + url += "&%s=%s" % (name, trans.encode_path(value[0], self.urlencoding))
1.168 +
1.169 + # Show the success page anyway.
1.170 +
1.171 + self.show_success(trans, url)
1.172 + if self.use_redirect:
1.173 + trans.redirect(url)
1.174 + else:
1.175 + raise WebStack.Generic.EndOfResponse
1.176 +
1.177 + def show_verification(self, trans, status):
1.178 +
1.179 + """
1.180 + Writes a signature verification response using the transaction 'trans'
1.181 + and the 'status' of the verification.
1.182 + """
1.183 +
1.184 + trans.set_content_type(WebStack.Generic.ContentType("text/plain"))
1.185 + out = trans.get_response_stream()
1.186 +
1.187 + # NOTE: Need to use invalidate_handle, too.
1.188 +
1.189 + if status:
1.190 + status_str = "true"
1.191 + else:
1.192 + status_str = "false"
1.193 + out.write("ns:%s\nis_valid:%s\n" % (self.openid_ns, status_str))
1.194 + raise WebStack.Generic.EndOfResponse
1.195 +
1.196 + def show_login(self, trans, app, claimed_id, local_id):
1.197 +
1.198 + """
1.199 + Writes a login screen using the transaction 'trans', including details
1.200 + of the 'app' which the client was attempting to access, along with the
1.201 + 'claimed_id' and 'local_id'.
1.202 + """
1.203 +
1.204 + trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding))
1.205 + out = trans.get_response_stream()
1.206 + out.write(self.login_page % (app, claimed_id, local_id))
1.207 +
1.208 + def show_success(self, trans, app):
1.209 +
1.210 + """
1.211 + Writes a success screen using the transaction 'trans', including details
1.212 + of the 'app' which the client was attempting to access.
1.213 + """
1.214 +
1.215 + trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding))
1.216 + out = trans.get_response_stream()
1.217 + out.write(self.success_page % (app, app))
1.218 +
1.219 + login_page = """
1.220 +<html>
1.221 + <head>
1.222 + <title>Login</title>
1.223 + </head>
1.224 + <body>
1.225 + <h1>Login</h1>
1.226 + <form method="POST">
1.227 + <p>Username: <input name="username" type="text" size="12"/></p>
1.228 + <p>Password: <input name="password" type="password" size="12"/></p>
1.229 + <p><input name="login" type="submit" value="Login"/></p>
1.230 + <input name="app" type="hidden" value="%s"/>
1.231 + <input name="claimed_id" type="hidden" value="%s"/>
1.232 + <input name="local_id" type="hidden" value="%s"/>
1.233 + </form>
1.234 + </body>
1.235 +</html>
1.236 +"""
1.237 +
1.238 + success_page = """
1.239 +<html>
1.240 + <head>
1.241 + <title>Login Example</title>
1.242 + </head>
1.243 + <body>
1.244 + <h1>Login Successful</h1>
1.245 + <p>Please proceed to the application: <a href="%s">%s</a></p>
1.246 + </body>
1.247 +</html>
1.248 +"""
1.249 +
1.250 +# vim: tabstop=4 expandtab shiftwidth=4