paulb@733 | 1 | #!/usr/bin/env python |
paulb@733 | 2 | |
paulb@733 | 3 | """ |
paulb@733 | 4 | OpenID provider login resources which redirect clients back to the application |
paulb@733 | 5 | ("relying party"). |
paulb@733 | 6 | |
paulb@756 | 7 | Copyright (C) 2004, 2005, 2006, 2007, 2008 Paul Boddie <paul@boddie.org.uk> |
paulb@733 | 8 | |
paulb@733 | 9 | This library is free software; you can redistribute it and/or |
paulb@733 | 10 | modify it under the terms of the GNU Lesser General Public |
paulb@733 | 11 | License as published by the Free Software Foundation; either |
paulb@733 | 12 | version 2.1 of the License, or (at your option) any later version. |
paulb@733 | 13 | |
paulb@733 | 14 | This library is distributed in the hope that it will be useful, |
paulb@733 | 15 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
paulb@733 | 16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
paulb@733 | 17 | Lesser General Public License for more details. |
paulb@733 | 18 | |
paulb@733 | 19 | You should have received a copy of the GNU Lesser General Public |
paulb@733 | 20 | License along with this library; if not, write to the Free Software |
paulb@733 | 21 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA |
paulb@733 | 22 | """ |
paulb@733 | 23 | |
paulb@733 | 24 | import WebStack.Generic |
paulb@733 | 25 | from WebStack.Helpers.Auth import Authenticator, check_openid_signature, make_openid_signature |
paulb@733 | 26 | import datetime |
paulb@733 | 27 | import time |
paulb@733 | 28 | import random |
paulb@740 | 29 | import cgi # for escape |
paulb@756 | 30 | import urlparse # for urlsplit |
paulb@733 | 31 | |
paulb@742 | 32 | class OpenIDLoginUtils: |
paulb@733 | 33 | |
paulb@742 | 34 | "Utilities for OpenID login screens which may be inherited." |
paulb@733 | 35 | |
paulb@742 | 36 | openid_ns = "http://specs.openid.net/auth/2.0" |
paulb@742 | 37 | signed_names = ["op_endpoint", "return_to", "response_nonce", "assoc_handle", "claimed_id", "identity"] |
paulb@733 | 38 | |
paulb@756 | 39 | def __init__(self, app_url, authenticator, associations=None, use_redirect=1, urlencoding=None): |
paulb@756 | 40 | |
paulb@756 | 41 | """ |
paulb@756 | 42 | Initialise the resource with the application URL 'app_url' and an |
paulb@759 | 43 | 'authenticator'. The 'app_url' should be the "bare" reference using a |
paulb@759 | 44 | protocol, host and port, not including any path information. |
paulb@756 | 45 | |
paulb@756 | 46 | The optional 'associations' is a mapping from association handles to |
paulb@756 | 47 | secret keys. |
paulb@756 | 48 | |
paulb@756 | 49 | If the optional 'use_redirect' flag is set to a false value (which is |
paulb@756 | 50 | not the default), a confirmation screen is given instead of immediately |
paulb@756 | 51 | redirecting the user back to the original application. |
paulb@756 | 52 | |
paulb@756 | 53 | The optional 'urlencoding' parameter allows a special encoding to be |
paulb@756 | 54 | used in producing the redirection path. |
paulb@756 | 55 | """ |
paulb@756 | 56 | |
paulb@756 | 57 | self.app_url = app_url |
paulb@756 | 58 | self.authenticator = authenticator |
paulb@733 | 59 | self.associations = associations or {} |
paulb@733 | 60 | self.use_redirect = use_redirect |
paulb@756 | 61 | self.urlencoding = urlencoding |
paulb@733 | 62 | |
paulb@742 | 63 | def get_openid_fields(self, trans, claimed_id, local_id, username, return_to, endpoint): |
paulb@733 | 64 | |
paulb@733 | 65 | # Make an association that can be used in signature verification. |
paulb@733 | 66 | # NOTE: Probably need to consider the secret key a bit more. |
paulb@733 | 67 | |
paulb@733 | 68 | handle = username + str(time.time()) |
paulb@733 | 69 | secret_key = str(random.randint(0, 999999999)) |
paulb@733 | 70 | self.associations[handle] = secret_key |
paulb@733 | 71 | |
paulb@733 | 72 | # Make a timestamp. |
paulb@733 | 73 | |
paulb@733 | 74 | now = datetime.datetime.utcnow() |
paulb@733 | 75 | timestamp = now.strftime("%Y-%m-%dT%H:%M:%SZ") + str(now.microsecond) |
paulb@733 | 76 | |
paulb@733 | 77 | # Make a signature. |
paulb@733 | 78 | |
paulb@733 | 79 | fields = { |
paulb@742 | 80 | "openid.ns" : [self.openid_ns], |
paulb@742 | 81 | "openid.mode" : ["id_res"], |
paulb@742 | 82 | "openid.signed" : [",".join(self.signed_names)], |
paulb@742 | 83 | "openid.op_endpoint" : [endpoint], |
paulb@742 | 84 | "openid.return_to" : [return_to], |
paulb@733 | 85 | "openid.response_nonce" : [timestamp], |
paulb@733 | 86 | "openid.assoc_handle" : [handle], |
paulb@733 | 87 | "openid.claimed_id" : [claimed_id], |
paulb@733 | 88 | "openid.identity" : [local_id] |
paulb@733 | 89 | } |
paulb@742 | 90 | signature = make_openid_signature(self.signed_names, fields, secret_key) |
paulb@742 | 91 | fields["openid.sig"] = [signature] |
paulb@733 | 92 | |
paulb@742 | 93 | return fields |
paulb@742 | 94 | |
paulb@742 | 95 | def get_openid_url(self, trans, fields): |
paulb@733 | 96 | |
paulb@733 | 97 | # Build an URL for returning to the application. |
paulb@733 | 98 | |
paul@776 | 99 | url = fields["openid.return_to"][0] |
paul@776 | 100 | if "?" in url: |
paul@776 | 101 | url += "&" |
paul@776 | 102 | else: |
paul@776 | 103 | url += "?" |
paulb@742 | 104 | |
paulb@742 | 105 | first = 1 |
paulb@742 | 106 | for name, value in fields.items(): |
paulb@742 | 107 | if not first: |
paulb@742 | 108 | url += "&" |
paulb@756 | 109 | url += "%s=%s" % (name, trans.encode_path(value[0])) |
paulb@742 | 110 | first = 0 |
paulb@733 | 111 | |
paulb@742 | 112 | return url |
paulb@742 | 113 | |
paulb@742 | 114 | def redirect_to_application(self, trans, claimed_id, local_id, username, return_to, endpoint): |
paulb@742 | 115 | |
paulb@742 | 116 | """ |
paulb@742 | 117 | Redirect the client using 'trans', 'claimed_id', 'local_id', 'username' |
paulb@742 | 118 | and the given 'return_to' and 'endpoint' details. |
paulb@742 | 119 | """ |
paulb@742 | 120 | |
paulb@742 | 121 | fields = self.get_openid_fields(trans, claimed_id, local_id, username, return_to, endpoint) |
paulb@742 | 122 | url = self.get_openid_url(trans, fields) |
paulb@733 | 123 | |
paulb@733 | 124 | # Show the success page anyway. |
paulb@738 | 125 | # Offer a POST-based form for redirection. |
paulb@733 | 126 | |
paulb@742 | 127 | self.show_success(trans, fields) |
paulb@733 | 128 | if self.use_redirect: |
paulb@733 | 129 | trans.redirect(url) |
paulb@733 | 130 | else: |
paulb@733 | 131 | raise WebStack.Generic.EndOfResponse |
paulb@733 | 132 | |
paulb@733 | 133 | def show_verification(self, trans, status): |
paulb@733 | 134 | |
paulb@733 | 135 | """ |
paulb@733 | 136 | Writes a signature verification response using the transaction 'trans' |
paulb@733 | 137 | and the 'status' of the verification. |
paulb@733 | 138 | """ |
paulb@733 | 139 | |
paulb@733 | 140 | trans.set_content_type(WebStack.Generic.ContentType("text/plain")) |
paulb@733 | 141 | out = trans.get_response_stream() |
paulb@733 | 142 | |
paulb@733 | 143 | # NOTE: Need to use invalidate_handle, too. |
paulb@733 | 144 | |
paulb@733 | 145 | if status: |
paulb@733 | 146 | status_str = "true" |
paulb@733 | 147 | else: |
paulb@733 | 148 | status_str = "false" |
paulb@733 | 149 | out.write("ns:%s\nis_valid:%s\n" % (self.openid_ns, status_str)) |
paulb@733 | 150 | raise WebStack.Generic.EndOfResponse |
paulb@733 | 151 | |
paulb@742 | 152 | def check_authentication(self, trans, fields): |
paulb@742 | 153 | |
paulb@742 | 154 | "Check the authentication details supplied in 'trans' and 'fields'." |
paulb@742 | 155 | |
paulb@742 | 156 | # Obtain the secret key from recorded associations. |
paulb@742 | 157 | |
paulb@742 | 158 | handle = fields.get("openid.assoc_handle", [None])[0] |
paulb@742 | 159 | if handle is not None and self.associations.has_key(handle): |
paulb@742 | 160 | valid = check_openid_signature(fields, self.associations[handle]) |
paulb@742 | 161 | del self.associations[handle] |
paulb@742 | 162 | else: |
paulb@742 | 163 | valid = 0 |
paulb@742 | 164 | |
paulb@742 | 165 | # Produce a response for this request. |
paulb@742 | 166 | |
paulb@742 | 167 | self.show_verification(trans, valid) |
paulb@742 | 168 | |
paulb@742 | 169 | class OpenIDLoginResource(OpenIDLoginUtils): |
paulb@742 | 170 | |
paulb@742 | 171 | "A resource providing a login screen." |
paulb@742 | 172 | |
paulb@742 | 173 | def __init__(self, app_url, authenticator, associations=None, use_redirect=1, urlencoding=None, encoding=None): |
paulb@733 | 174 | |
paulb@733 | 175 | """ |
paulb@742 | 176 | Initialise the resource with the application URL 'app_url' and an |
paulb@742 | 177 | 'authenticator'. |
paulb@742 | 178 | |
paulb@742 | 179 | The optional 'associations' is a mapping from association handles to |
paulb@742 | 180 | secret keys. |
paulb@742 | 181 | |
paulb@742 | 182 | If the optional 'use_redirect' flag is set to a false value (which is |
paulb@742 | 183 | not the default), a confirmation screen is given instead of immediately |
paulb@742 | 184 | redirecting the user back to the original application. |
paulb@742 | 185 | |
paulb@742 | 186 | The optional 'urlencoding' parameter allows a special encoding to be |
paulb@742 | 187 | used in producing the redirection path. |
paulb@742 | 188 | |
paulb@742 | 189 | The optional 'encoding' parameter allows a special encoding to be used |
paulb@742 | 190 | in producing the login pages. |
paulb@742 | 191 | |
paulb@742 | 192 | To change the pages employed by this resource, either redefine the |
paulb@742 | 193 | 'login_page' and 'success_page' attributes in instances of this class or |
paulb@742 | 194 | a subclass, or override the 'show_login' and 'show_success' methods. |
paulb@733 | 195 | """ |
paulb@733 | 196 | |
paulb@756 | 197 | OpenIDLoginUtils.__init__(self, app_url, authenticator, associations, use_redirect, urlencoding) |
paulb@752 | 198 | self.encoding = encoding |
paulb@742 | 199 | |
paulb@742 | 200 | def respond(self, trans): |
paulb@742 | 201 | |
paulb@742 | 202 | "Respond using the transaction 'trans'." |
paulb@742 | 203 | |
paulb@742 | 204 | # Check for a submitted login form. |
paulb@742 | 205 | |
paulb@742 | 206 | fields = trans.get_fields(self.encoding) |
paulb@742 | 207 | |
paulb@742 | 208 | if fields.has_key("login"): |
paulb@742 | 209 | self.check_login(trans, fields) |
paulb@742 | 210 | # The above method may not return. |
paulb@742 | 211 | |
paulb@742 | 212 | # Check for an OpenID signature verification request. |
paulb@742 | 213 | |
paulb@742 | 214 | elif fields.get("openid.mode", [None])[0] == "check_authentication": |
paulb@742 | 215 | self.check_authentication(trans, fields) |
paulb@742 | 216 | # The above method does not return. |
paulb@742 | 217 | |
paulb@742 | 218 | # NOTE: Permit association requests here. |
paulb@742 | 219 | # Otherwise, show the login form. |
paulb@742 | 220 | |
paulb@742 | 221 | self.show_login(trans, fields) |
paulb@742 | 222 | |
paulb@756 | 223 | def check_login(self, trans, fields): |
paulb@756 | 224 | |
paulb@756 | 225 | "Check the login details supplied in 'trans' and 'fields'." |
paulb@756 | 226 | |
paulb@756 | 227 | return_to = fields.get("openid.return_to", [""])[0] |
paulb@756 | 228 | claimed_id = fields.get("openid.claimed_id", [""])[0] |
paulb@756 | 229 | local_id = fields.get("openid.identity", [""])[0] |
paulb@756 | 230 | |
paulb@756 | 231 | # Check a combination of local identifier and username together with |
paulb@756 | 232 | # the password. |
paulb@756 | 233 | |
paulb@756 | 234 | username = fields.get("username", [""])[0] |
paulb@756 | 235 | password = fields.get("password", [""])[0] |
paulb@756 | 236 | |
paulb@756 | 237 | # NOTE: Permit flexibility in the credentials. |
paulb@756 | 238 | |
paulb@756 | 239 | if self.authenticator.authenticate(trans, (local_id, username), password): |
paulb@756 | 240 | endpoint = self.app_url + trans.get_path_without_query(self.urlencoding) |
paulb@756 | 241 | self.redirect_to_application(trans, claimed_id, local_id, username, return_to, endpoint) |
paulb@756 | 242 | |
paulb@742 | 243 | def show_login(self, trans, fields): |
paulb@742 | 244 | |
paulb@742 | 245 | """ |
paulb@742 | 246 | Writes a login screen using the transaction 'trans' and 'fields'. |
paulb@742 | 247 | """ |
paulb@742 | 248 | |
paulb@742 | 249 | return_to = fields.get("openid.return_to", [""])[0] |
paulb@742 | 250 | claimed_id = fields.get("openid.claimed_id", [""])[0] |
paulb@742 | 251 | local_id = fields.get("openid.identity", [""])[0] |
paulb@742 | 252 | |
paulb@756 | 253 | trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding or trans.default_charset)) |
paulb@733 | 254 | out = trans.get_response_stream() |
paulb@742 | 255 | out.write(self.login_page % tuple(map(cgi.escape, (return_to, claimed_id, local_id)))) |
paulb@733 | 256 | |
paulb@742 | 257 | def show_success(self, trans, fields): |
paulb@733 | 258 | |
paulb@733 | 259 | """ |
paulb@742 | 260 | Writes a success screen using the transaction 'trans', using a |
paulb@742 | 261 | dictionary of 'fields' providing details of the transaction. |
paulb@733 | 262 | """ |
paulb@733 | 263 | |
paulb@756 | 264 | trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding or trans.default_charset)) |
paulb@733 | 265 | out = trans.get_response_stream() |
paulb@738 | 266 | l = [] |
paulb@742 | 267 | for name, values in fields.items(): |
paulb@742 | 268 | l.append("""<input name="%s" type="hidden" value="%s" />""" % (name, cgi.escape(values[0]))) |
paulb@742 | 269 | out.write(self.success_page % (fields["openid.return_to"][0], "\n".join(l))) |
paulb@733 | 270 | |
paulb@733 | 271 | login_page = """ |
paulb@733 | 272 | <html> |
paulb@733 | 273 | <head> |
paulb@733 | 274 | <title>Login</title> |
paulb@733 | 275 | </head> |
paulb@733 | 276 | <body> |
paulb@733 | 277 | <h1>Login</h1> |
paulb@733 | 278 | <form method="POST"> |
paulb@733 | 279 | <p>Username: <input name="username" type="text" size="12"/></p> |
paulb@733 | 280 | <p>Password: <input name="password" type="password" size="12"/></p> |
paulb@733 | 281 | <p><input name="login" type="submit" value="Login"/></p> |
paulb@740 | 282 | <input name="openid.return_to" type="hidden" value="%s"/> |
paulb@740 | 283 | <input name="openid.claimed_id" type="hidden" value="%s"/> |
paulb@740 | 284 | <input name="openid.identity" type="hidden" value="%s"/> |
paulb@733 | 285 | </form> |
paulb@733 | 286 | </body> |
paulb@733 | 287 | </html> |
paulb@733 | 288 | """ |
paulb@733 | 289 | |
paulb@733 | 290 | success_page = """ |
paulb@733 | 291 | <html> |
paulb@733 | 292 | <head> |
paulb@733 | 293 | <title>Login Example</title> |
paulb@733 | 294 | </head> |
paulb@733 | 295 | <body> |
paulb@733 | 296 | <h1>Login Successful</h1> |
paulb@738 | 297 | <form action="%s" method="POST" name="openid_redirect"> |
paulb@738 | 298 | %s |
paulb@738 | 299 | <p>Please proceed to the application: <input name="proceed" type="submit" value="Proceed!" /></p> |
paulb@738 | 300 | </form> |
paulb@733 | 301 | </body> |
paulb@733 | 302 | </html> |
paulb@733 | 303 | """ |
paulb@733 | 304 | |
paulb@733 | 305 | # vim: tabstop=4 expandtab shiftwidth=4 |