2.1 --- a/WebStack/Resources/OpenIDLogin.py Mon Nov 19 00:23:06 2007 +0000
2.2 +++ b/WebStack/Resources/OpenIDLogin.py Mon Nov 19 00:23:45 2007 +0000
2.3 @@ -28,102 +28,24 @@
2.4 import random
2.5 import cgi # for escape
2.6
2.7 -class OpenIDLoginResource:
2.8 -
2.9 - "A resource providing a login screen."
2.10 -
2.11 - encoding = "utf-8"
2.12 - openid_ns = "http://specs.openid.net/auth/2.0"
2.13 +class OpenIDLoginUtils:
2.14
2.15 - def __init__(self, app_url, authenticator, associations=None, use_redirect=0, urlencoding=None, encoding=None):
2.16 -
2.17 - """
2.18 - Initialise the resource with the application URL 'app_url' and an
2.19 - 'authenticator'.
2.20 -
2.21 - The optional 'associations' is a mapping from association handles to
2.22 - secret keys.
2.23 + "Utilities for OpenID login screens which may be inherited."
2.24
2.25 - If the optional 'use_redirect' flag is set to a false value (which is
2.26 - not the default), a confirmation screen is given instead of immediately
2.27 - redirecting the user back to the original application.
2.28 -
2.29 - The optional 'urlencoding' parameter allows a special encoding to be
2.30 - used in producing the redirection path.
2.31 + openid_ns = "http://specs.openid.net/auth/2.0"
2.32 + signed_names = ["op_endpoint", "return_to", "response_nonce", "assoc_handle", "claimed_id", "identity"]
2.33
2.34 - The optional 'encoding' parameter allows a special encoding to be used
2.35 - in producing the login pages.
2.36 -
2.37 - To change the pages employed by this resource, either redefine the
2.38 - 'login_page' and 'success_page' attributes in instances of this class or
2.39 - a subclass, or override the 'show_login' and 'show_success' methods.
2.40 - """
2.41 -
2.42 - self.app_url = app_url
2.43 - self.authenticator = authenticator
2.44 + def __init__(self, associations=None, use_redirect=1):
2.45 self.associations = associations or {}
2.46 self.use_redirect = use_redirect
2.47 - self.urlencoding = urlencoding
2.48 - self.encoding = encoding or self.encoding
2.49 -
2.50 - def respond(self, trans):
2.51 -
2.52 - "Respond using the transaction 'trans'."
2.53 -
2.54 - # Check for a submitted login form.
2.55 -
2.56 - fields = trans.get_fields(self.encoding)
2.57 -
2.58 - app = fields.get("openid.return_to", [""])[0]
2.59 - claimed_id = fields.get("openid.claimed_id", [""])[0]
2.60 - local_id = fields.get("openid.identity", [""])[0]
2.61 -
2.62 - if fields.has_key("login"):
2.63 -
2.64 - # Check a combination of local identifier and username together with
2.65 - # the password.
2.66 -
2.67 - username = fields.get("username", [""])[0]
2.68 - password = fields.get("password", [""])[0]
2.69 -
2.70 - # NOTE: Permit flexibility in the credentials.
2.71 -
2.72 - if self.authenticator.authenticate(trans, (local_id, username), password):
2.73 - self._redirect(trans, claimed_id, local_id, username, app)
2.74 - # The above method does not return.
2.75 -
2.76 - # Check for an OpenID signature verification request.
2.77
2.78 - elif fields.get("openid.mode", [None])[0] == "check_authentication":
2.79 -
2.80 - # Obtain the secret key from recorded associations.
2.81 -
2.82 - handle = fields.get("openid.assoc_handle", [None])[0]
2.83 - if handle is not None and self.associations.has_key(handle):
2.84 - valid = check_openid_signature(fields, self.associations[handle])
2.85 - del self.associations[handle]
2.86 - else:
2.87 - valid = 0
2.88 -
2.89 - # Produce a response for this request.
2.90 + def urlencode(self, trans, value):
2.91 + if not trans.default_charset:
2.92 + return trans.encode_path(value, self.urlencoding)
2.93 + else:
2.94 + return trans.encode_path(value)
2.95
2.96 - self.show_verification(trans, valid)
2.97 - # The above method does not return.
2.98 -
2.99 - # NOTE: Permit association requests here.
2.100 -
2.101 - # Otherwise, show the login form.
2.102 -
2.103 - self.show_login(trans, app, claimed_id, local_id)
2.104 -
2.105 - def _redirect(self, trans, claimed_id, local_id, username, app):
2.106 -
2.107 - """
2.108 - Redirect the client using 'trans', 'claimed_id', 'local_id', 'username'
2.109 - and the given 'app' details.
2.110 - """
2.111 -
2.112 - app_url = self.app_url + trans.get_path_without_query(self.urlencoding)
2.113 + def get_openid_fields(self, trans, claimed_id, local_id, username, return_to, endpoint):
2.114
2.115 # Make an association that can be used in signature verification.
2.116 # NOTE: Probably need to consider the secret key a bit more.
2.117 @@ -139,35 +61,51 @@
2.118
2.119 # Make a signature.
2.120
2.121 - signed_names = ["op_endpoint", "return_to", "response_nonce", "assoc_handle", "claimed_id", "identity"]
2.122 fields = {
2.123 - "openid.op_endpoint" : [app_url],
2.124 - "openid.return_to" : [app],
2.125 + "openid.ns" : [self.openid_ns],
2.126 + "openid.mode" : ["id_res"],
2.127 + "openid.signed" : [",".join(self.signed_names)],
2.128 + "openid.op_endpoint" : [endpoint],
2.129 + "openid.return_to" : [return_to],
2.130 "openid.response_nonce" : [timestamp],
2.131 "openid.assoc_handle" : [handle],
2.132 "openid.claimed_id" : [claimed_id],
2.133 "openid.identity" : [local_id]
2.134 }
2.135 + signature = make_openid_signature(self.signed_names, fields, secret_key)
2.136 + fields["openid.sig"] = [signature]
2.137
2.138 - signature = make_openid_signature(signed_names, fields, secret_key)
2.139 + return fields
2.140 +
2.141 + def get_openid_url(self, trans, fields):
2.142
2.143 # Build an URL for returning to the application.
2.144
2.145 - url = "%s?openid.ns=%s&openid.mode=%s&openid.signed=%s&openid.sig=%s" % (
2.146 - app,
2.147 - trans.encode_path(self.openid_ns, self.urlencoding),
2.148 - trans.encode_path("id_res", self.urlencoding),
2.149 - trans.encode_path(",".join(signed_names), self.urlencoding),
2.150 - trans.encode_path(signature, self.urlencoding)
2.151 - )
2.152 + url = "%s?" % fields["openid.return_to"][0]
2.153 +
2.154 + first = 1
2.155 + for name, value in fields.items():
2.156 + if not first:
2.157 + url += "&"
2.158 + url += "%s=%s" % (name, self.urlencode(trans, value[0]))
2.159 + first = 0
2.160
2.161 - for name, value in fields.items():
2.162 - url += "&%s=%s" % (name, trans.encode_path(value[0], self.urlencoding))
2.163 + return url
2.164 +
2.165 + def redirect_to_application(self, trans, claimed_id, local_id, username, return_to, endpoint):
2.166 +
2.167 + """
2.168 + Redirect the client using 'trans', 'claimed_id', 'local_id', 'username'
2.169 + and the given 'return_to' and 'endpoint' details.
2.170 + """
2.171 +
2.172 + fields = self.get_openid_fields(trans, claimed_id, local_id, username, return_to, endpoint)
2.173 + url = self.get_openid_url(trans, fields)
2.174
2.175 # Show the success page anyway.
2.176 # Offer a POST-based form for redirection.
2.177
2.178 - self.show_success(trans, app, "id_res", signed_names, signature, fields)
2.179 + self.show_success(trans, fields)
2.180 if self.use_redirect:
2.181 trans.redirect(url)
2.182 else:
2.183 @@ -192,37 +130,129 @@
2.184 out.write("ns:%s\nis_valid:%s\n" % (self.openid_ns, status_str))
2.185 raise WebStack.Generic.EndOfResponse
2.186
2.187 - def show_login(self, trans, app, claimed_id, local_id):
2.188 + def check_authentication(self, trans, fields):
2.189 +
2.190 + "Check the authentication details supplied in 'trans' and 'fields'."
2.191 +
2.192 + # Obtain the secret key from recorded associations.
2.193 +
2.194 + handle = fields.get("openid.assoc_handle", [None])[0]
2.195 + if handle is not None and self.associations.has_key(handle):
2.196 + valid = check_openid_signature(fields, self.associations[handle])
2.197 + del self.associations[handle]
2.198 + else:
2.199 + valid = 0
2.200 +
2.201 + # Produce a response for this request.
2.202 +
2.203 + self.show_verification(trans, valid)
2.204 +
2.205 + def check_login(self, trans, fields):
2.206 +
2.207 + "Check the login details supplied in 'trans' and 'fields'."
2.208 +
2.209 + return_to = fields.get("openid.return_to", [""])[0]
2.210 + claimed_id = fields.get("openid.claimed_id", [""])[0]
2.211 + local_id = fields.get("openid.identity", [""])[0]
2.212 +
2.213 + # Check a combination of local identifier and username together with
2.214 + # the password.
2.215 +
2.216 + username = fields.get("username", [""])[0]
2.217 + password = fields.get("password", [""])[0]
2.218 +
2.219 + # NOTE: Permit flexibility in the credentials.
2.220 +
2.221 + if self.authenticator.authenticate(trans, (local_id, username), password):
2.222 + endpoint = self.app_url + trans.get_path_without_query(self.urlencoding)
2.223 + self.redirect_to_application(trans, claimed_id, local_id, username, return_to, endpoint)
2.224 +
2.225 +class OpenIDLoginResource(OpenIDLoginUtils):
2.226 +
2.227 + "A resource providing a login screen."
2.228 +
2.229 + encoding = "utf-8"
2.230 +
2.231 + def __init__(self, app_url, authenticator, associations=None, use_redirect=1, urlencoding=None, encoding=None):
2.232
2.233 """
2.234 - Writes a login screen using the transaction 'trans', including details
2.235 - of the 'app' which the client was attempting to access, along with the
2.236 - 'claimed_id' and 'local_id'.
2.237 + Initialise the resource with the application URL 'app_url' and an
2.238 + 'authenticator'.
2.239 +
2.240 + The optional 'associations' is a mapping from association handles to
2.241 + secret keys.
2.242 +
2.243 + If the optional 'use_redirect' flag is set to a false value (which is
2.244 + not the default), a confirmation screen is given instead of immediately
2.245 + redirecting the user back to the original application.
2.246 +
2.247 + The optional 'urlencoding' parameter allows a special encoding to be
2.248 + used in producing the redirection path.
2.249 +
2.250 + The optional 'encoding' parameter allows a special encoding to be used
2.251 + in producing the login pages.
2.252 +
2.253 + To change the pages employed by this resource, either redefine the
2.254 + 'login_page' and 'success_page' attributes in instances of this class or
2.255 + a subclass, or override the 'show_login' and 'show_success' methods.
2.256 """
2.257
2.258 + OpenIDLoginUtils.__init__(self, associations, use_redirect)
2.259 + self.app_url = app_url
2.260 + self.authenticator = authenticator
2.261 + self.urlencoding = urlencoding
2.262 + self.encoding = encoding or self.encoding
2.263 +
2.264 + def respond(self, trans):
2.265 +
2.266 + "Respond using the transaction 'trans'."
2.267 +
2.268 + # Check for a submitted login form.
2.269 +
2.270 + fields = trans.get_fields(self.encoding)
2.271 +
2.272 + if fields.has_key("login"):
2.273 + self.check_login(trans, fields)
2.274 + # The above method may not return.
2.275 +
2.276 + # Check for an OpenID signature verification request.
2.277 +
2.278 + elif fields.get("openid.mode", [None])[0] == "check_authentication":
2.279 + self.check_authentication(trans, fields)
2.280 + # The above method does not return.
2.281 +
2.282 + # NOTE: Permit association requests here.
2.283 + # Otherwise, show the login form.
2.284 +
2.285 + self.show_login(trans, fields)
2.286 +
2.287 + def show_login(self, trans, fields):
2.288 +
2.289 + """
2.290 + Writes a login screen using the transaction 'trans' and 'fields'.
2.291 + """
2.292 +
2.293 + return_to = fields.get("openid.return_to", [""])[0]
2.294 + claimed_id = fields.get("openid.claimed_id", [""])[0]
2.295 + local_id = fields.get("openid.identity", [""])[0]
2.296 +
2.297 trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding))
2.298 out = trans.get_response_stream()
2.299 - out.write(self.login_page % tuple(map(cgi.escape, (app, claimed_id, local_id))))
2.300 + out.write(self.login_page % tuple(map(cgi.escape, (return_to, claimed_id, local_id))))
2.301
2.302 - def show_success(self, trans, app, mode, signed_names, signature, fields):
2.303 + def show_success(self, trans, fields):
2.304
2.305 """
2.306 - Writes a success screen using the transaction 'trans', including details
2.307 - of the 'app' which the client was attempting to access, the
2.308 - communications 'mode', the 'signed_names' indicating the fields which
2.309 - are signed, the 'signature' associated with the message, and a
2.310 - dictionary of 'fields' indicating other information.
2.311 + Writes a success screen using the transaction 'trans', using a
2.312 + dictionary of 'fields' providing details of the transaction.
2.313 """
2.314
2.315 trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding))
2.316 out = trans.get_response_stream()
2.317 l = []
2.318 - for name, value in fields.items():
2.319 - l.append("""<input name="%s" type="hidden" value="%s" />""" % (name, value[0]))
2.320 - args = tuple(
2.321 - map(cgi.escape, (app, self.openid_ns, mode, ",".join(signed_names), signature))
2.322 - ) + ("\n".join(l),)
2.323 - out.write(self.success_page % args)
2.324 + for name, values in fields.items():
2.325 + l.append("""<input name="%s" type="hidden" value="%s" />""" % (name, cgi.escape(values[0])))
2.326 + out.write(self.success_page % (fields["openid.return_to"][0], "\n".join(l)))
2.327
2.328 login_page = """
2.329 <html>
2.330 @@ -251,10 +281,6 @@
2.331 <body>
2.332 <h1>Login Successful</h1>
2.333 <form action="%s" method="POST" name="openid_redirect">
2.334 - <input name="openid.ns" type="hidden" value="%s" />
2.335 - <input name="openid.mode" type="hidden" value="%s" />
2.336 - <input name="openid.signed" type="hidden" value="%s" />
2.337 - <input name="openid.sig" type="hidden" value="%s" />
2.338 %s
2.339 <p>Please proceed to the application: <input name="proceed" type="submit" value="Proceed!" /></p>
2.340 </form>