paulb@733 | 1 | #!/usr/bin/env python |
paulb@733 | 2 | |
paulb@733 | 3 | """ |
paulb@733 | 4 | OpenID initiation resources which redirect clients to an OpenID provider. |
paulb@733 | 5 | |
paulb@756 | 6 | Copyright (C) 2004, 2005, 2006, 2007, 2008 Paul Boddie <paul@boddie.org.uk> |
paulb@733 | 7 | |
paulb@733 | 8 | This library is free software; you can redistribute it and/or |
paulb@733 | 9 | modify it under the terms of the GNU Lesser General Public |
paulb@733 | 10 | License as published by the Free Software Foundation; either |
paulb@733 | 11 | version 2.1 of the License, or (at your option) any later version. |
paulb@733 | 12 | |
paulb@733 | 13 | This library is distributed in the hope that it will be useful, |
paulb@733 | 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
paulb@733 | 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
paulb@733 | 16 | Lesser General Public License for more details. |
paulb@733 | 17 | |
paulb@733 | 18 | You should have received a copy of the GNU Lesser General Public |
paulb@733 | 19 | License along with this library; if not, write to the Free Software |
paulb@733 | 20 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA |
paulb@733 | 21 | """ |
paulb@733 | 22 | |
paulb@733 | 23 | import WebStack.Generic |
paulb@733 | 24 | import libxml2dom |
paulb@740 | 25 | import cgi # for escape |
paulb@733 | 26 | |
paulb@756 | 27 | class OpenIDInitiationUtils: |
paulb@756 | 28 | |
paulb@756 | 29 | "Utilities for OpenID initiation screens which may be inherited." |
paulb@756 | 30 | |
paulb@756 | 31 | openid_ns = "http://specs.openid.net/auth/2.0" |
paulb@756 | 32 | |
paulb@756 | 33 | def __init__(self, openid_mode=None, use_redirect=1, urlencoding=None): |
paulb@756 | 34 | |
paulb@756 | 35 | """ |
paulb@756 | 36 | Initialise the resource. |
paulb@756 | 37 | |
paulb@756 | 38 | The optional 'openid_mode' parameter may be set to "checkid_immediate" |
paulb@756 | 39 | or "checkid_setup" (the default). |
paulb@756 | 40 | |
paulb@756 | 41 | If the optional 'use_redirect' flag is set to a false value (which is |
paulb@756 | 42 | not the default), a confirmation screen is given instead of immediately |
paulb@756 | 43 | redirecting the user to the OpenID provider. |
paulb@756 | 44 | |
paulb@756 | 45 | The optional 'urlencoding' parameter allows a special encoding to be |
paulb@756 | 46 | used in producing the redirection path. |
paulb@756 | 47 | """ |
paulb@756 | 48 | |
paulb@756 | 49 | self.openid_mode = openid_mode or "checkid_setup" |
paulb@756 | 50 | self.use_redirect = use_redirect |
paulb@756 | 51 | self.urlencoding = urlencoding |
paulb@756 | 52 | |
paulb@756 | 53 | def get_redirect_url(self, trans, app, claimed_identifier, provider, local_identifier): |
paulb@756 | 54 | |
paulb@756 | 55 | # NOTE: Should consider the special "select" mode for identity. |
paulb@756 | 56 | |
paulb@756 | 57 | return "%s?openid.ns=%s&openid.mode=%s&openid.return_to=%s&openid.claimed_id=%s&openid.identity=%s" % ( |
paulb@756 | 58 | trans.encode_url_without_query(provider, self.urlencoding), |
paulb@756 | 59 | trans.encode_path(self.openid_ns, self.urlencoding), |
paulb@756 | 60 | trans.encode_path(self.openid_mode, self.urlencoding), |
paulb@756 | 61 | trans.encode_path(app, self.urlencoding), |
paulb@756 | 62 | trans.encode_path(claimed_identifier, self.urlencoding), |
paulb@756 | 63 | trans.encode_path(local_identifier, self.urlencoding) |
paulb@756 | 64 | ) |
paulb@756 | 65 | |
paulb@756 | 66 | def get_provider_url(self, trans, identity): |
paulb@756 | 67 | |
paulb@756 | 68 | """ |
paulb@756 | 69 | Return the claimed identifier, provider URL and local identifier for the |
paulb@756 | 70 | authenticating user using the given 'trans' and 'identity'. |
paulb@756 | 71 | |
paulb@756 | 72 | See: |
paulb@756 | 73 | http://openid.net/specs/openid-authentication-2_0-12.html#rfc.section.7.3 |
paulb@756 | 74 | """ |
paulb@756 | 75 | |
paulb@756 | 76 | if identity.startswith("xri://"): |
paulb@756 | 77 | identity = openid[6:] |
paulb@756 | 78 | |
paul@775 | 79 | # Detect empty identifiers. |
paul@775 | 80 | |
paul@775 | 81 | if not identity: |
paul@775 | 82 | return None, None, None |
paul@775 | 83 | |
paulb@756 | 84 | # NOTE: Not yet discovering XRI providers. |
paulb@756 | 85 | |
paul@775 | 86 | elif identity[0] in ("=", "@", "+", "$", "!", "("): |
paulb@756 | 87 | pass |
paul@775 | 88 | |
paul@775 | 89 | # Handle URL-based identifiers. |
paul@775 | 90 | |
paulb@756 | 91 | else: |
paulb@756 | 92 | if not identity.startswith("http"): |
paulb@756 | 93 | identity = "http://" + identity |
paulb@756 | 94 | |
paulb@756 | 95 | # Obtain a provider url from a resource at the stated URL. |
paulb@756 | 96 | |
paulb@756 | 97 | doc = libxml2dom.parseURI(trans.encode_url_without_query(identity), html=1) |
paulb@756 | 98 | provider_links = doc.xpath("/html/head/link[contains(@rel, 'openid2.provider')]/@href") |
paulb@756 | 99 | local_ids = doc.xpath("/html/head/link[contains(@rel, 'openid2.local_id')]/@href") |
paulb@756 | 100 | if provider_links: |
paulb@756 | 101 | if local_ids: |
paulb@756 | 102 | return identity, provider_links[0].nodeValue, local_ids[0].nodeValue |
paulb@756 | 103 | else: |
paulb@756 | 104 | return identity, provider_links[0].nodeValue, None |
paulb@756 | 105 | |
paulb@756 | 106 | return identity, None, None |
paulb@756 | 107 | |
paulb@756 | 108 | class OpenIDInitiationResource(OpenIDInitiationUtils): |
paulb@733 | 109 | |
paulb@733 | 110 | "A resource providing an OpenID initiation screen." |
paulb@733 | 111 | |
paulb@742 | 112 | def __init__(self, openid_mode=None, use_redirect=1, urlencoding=None, encoding=None): |
paulb@733 | 113 | |
paulb@733 | 114 | """ |
paulb@733 | 115 | Initialise the resource. |
paulb@733 | 116 | |
paulb@733 | 117 | The optional 'openid_mode' parameter may be set to "checkid_immediate" |
paulb@733 | 118 | or "checkid_setup" (the default). |
paulb@733 | 119 | |
paulb@738 | 120 | If the optional 'use_redirect' flag is set to a false value (which is |
paulb@738 | 121 | not the default), a confirmation screen is given instead of immediately |
paulb@738 | 122 | redirecting the user to the OpenID provider. |
paulb@733 | 123 | |
paulb@733 | 124 | The optional 'urlencoding' parameter allows a special encoding to be |
paulb@733 | 125 | used in producing the redirection path. |
paulb@733 | 126 | |
paulb@733 | 127 | The optional 'encoding' parameter allows a special encoding to be used |
paulb@733 | 128 | in producing the initiation pages. |
paulb@733 | 129 | |
paulb@733 | 130 | To change the pages employed by this resource, either redefine the |
paulb@733 | 131 | 'initiation_page' and 'success_page' attributes in instances of this class or |
paulb@733 | 132 | a subclass, or override the 'show_initiation' and 'show_success' methods. |
paulb@733 | 133 | """ |
paulb@733 | 134 | |
paulb@756 | 135 | OpenIDInitiationUtils.__init__(self, openid_mode, use_redirect, urlencoding) |
paulb@752 | 136 | self.encoding = encoding |
paulb@733 | 137 | |
paulb@733 | 138 | def respond(self, trans): |
paulb@733 | 139 | |
paulb@733 | 140 | "Respond using the transaction 'trans'." |
paulb@733 | 141 | |
paulb@733 | 142 | app = get_target(trans, self.urlencoding, self.encoding) |
paulb@733 | 143 | |
paulb@733 | 144 | # Check for a submitted initiation form. |
paulb@733 | 145 | |
paulb@733 | 146 | fields_body = trans.get_fields_from_body(self.encoding) |
paulb@733 | 147 | |
paulb@733 | 148 | if fields_body.has_key("initiate") and fields_body.has_key("identity"): |
paulb@756 | 149 | self.check_identity(trans, fields_body, app) |
paulb@756 | 150 | # The above method does not return. |
paulb@733 | 151 | |
paulb@733 | 152 | # Otherwise, show the initiation form. |
paulb@733 | 153 | |
paulb@733 | 154 | self.show_initiation(trans, app) |
paulb@733 | 155 | |
paulb@756 | 156 | def check_identity(self, trans, fields, app): |
paulb@756 | 157 | |
paulb@756 | 158 | """ |
paulb@756 | 159 | Check the identity found through 'trans' and 'fields', using 'app' and |
paulb@756 | 160 | discovered information about the identity to redirect to the provider. |
paulb@756 | 161 | """ |
paulb@756 | 162 | |
paulb@756 | 163 | claimed_identifier, provider, local_identifier = self.get_provider_url(trans, fields["identity"][0]) |
paulb@756 | 164 | if provider is not None: |
paulb@756 | 165 | self.redirect_to_provider(trans, app, claimed_identifier, provider, local_identifier) |
paulb@756 | 166 | |
paulb@756 | 167 | def redirect_to_provider(self, trans, app, claimed_identifier, provider, local_identifier): |
paulb@733 | 168 | |
paulb@733 | 169 | """ |
paulb@733 | 170 | Redirect the client using 'trans' and the given 'app', |
paulb@733 | 171 | 'claimed_identifier', 'provider' and 'local_identifier' details. |
paulb@733 | 172 | |
paulb@733 | 173 | See: |
paulb@733 | 174 | http://openid.net/specs/openid-authentication-2_0-12.html#rfc.section.5.2 |
paulb@733 | 175 | http://openid.net/specs/openid-authentication-2_0-12.html#rfc.section.9 |
paulb@733 | 176 | """ |
paulb@733 | 177 | |
paulb@756 | 178 | url = self.get_redirect_url(trans, app, claimed_identifier, provider, local_identifier) |
paulb@733 | 179 | |
paulb@733 | 180 | # Show the success page anyway. |
paulb@738 | 181 | # Offer a POST-based form for redirection. |
paulb@733 | 182 | |
paulb@738 | 183 | self.show_success(trans, provider, app, claimed_identifier, local_identifier) |
paulb@733 | 184 | |
paulb@733 | 185 | # Redirect to the OpenID provider URL. |
paulb@733 | 186 | |
paulb@733 | 187 | if self.use_redirect: |
paulb@733 | 188 | trans.redirect(url) |
paulb@733 | 189 | else: |
paulb@733 | 190 | raise WebStack.Generic.EndOfResponse |
paulb@733 | 191 | |
paulb@733 | 192 | def show_initiation(self, trans, app): |
paulb@733 | 193 | |
paulb@733 | 194 | """ |
paulb@733 | 195 | Writes a initiation screen using the transaction 'trans', including details |
paulb@733 | 196 | of the 'app' which the client was attempting to access. |
paulb@733 | 197 | """ |
paulb@733 | 198 | |
paulb@756 | 199 | trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding or trans.default_charset)) |
paulb@733 | 200 | out = trans.get_response_stream() |
paulb@740 | 201 | out.write(self.initiation_page % cgi.escape(app)) |
paulb@733 | 202 | |
paulb@738 | 203 | def show_success(self, trans, provider, app, claimed_identifier, local_identifier): |
paulb@733 | 204 | |
paulb@733 | 205 | """ |
paulb@733 | 206 | Writes a success screen using the transaction 'trans', including details |
paulb@738 | 207 | of the OpenID 'provider', the 'app' URL, 'claimed_identifier' and |
paulb@738 | 208 | 'local_identifier'. |
paulb@733 | 209 | """ |
paulb@733 | 210 | |
paulb@756 | 211 | trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding or trans.default_charset)) |
paulb@733 | 212 | out = trans.get_response_stream() |
paulb@740 | 213 | out.write(self.success_page % tuple(map(cgi.escape, ( |
paulb@738 | 214 | provider, self.openid_ns, self.openid_mode, app, claimed_identifier, local_identifier) |
paulb@740 | 215 | ))) |
paulb@733 | 216 | |
paulb@733 | 217 | initiation_page = """ |
paulb@733 | 218 | <html> |
paulb@733 | 219 | <head> |
paulb@733 | 220 | <title>Authenticate via OpenID</title> |
paulb@733 | 221 | </head> |
paulb@733 | 222 | <body> |
paulb@733 | 223 | <h1>Authenticate via OpenID</h1> |
paulb@733 | 224 | <form method="POST" name="openid_identifier"> |
paulb@733 | 225 | <p>OpenID Identifier (URL): <input name="identity" type="text" size="32"/></p> |
paulb@738 | 226 | <p><input name="initiate" type="submit" value="Login" /></p> |
paulb@738 | 227 | <input name="app" type="hidden" value="%s" /> |
paulb@733 | 228 | </form> |
paulb@733 | 229 | </body> |
paulb@733 | 230 | </html> |
paulb@733 | 231 | """ |
paulb@733 | 232 | |
paulb@733 | 233 | success_page = """ |
paulb@733 | 234 | <html> |
paulb@733 | 235 | <head> |
paulb@733 | 236 | <title>Authenticate via OpenID</title> |
paulb@733 | 237 | </head> |
paulb@733 | 238 | <body> |
paulb@733 | 239 | <h1>Authenticate via OpenID</h1> |
paulb@738 | 240 | <form action="%s" method="POST" name="openid_redirect"> |
paulb@738 | 241 | <input name="openid.ns" type="hidden" value="%s" /> |
paulb@738 | 242 | <input name="openid.mode" type="hidden" value="%s" /> |
paulb@738 | 243 | <input name="openid.return_to" type="hidden" value="%s" /> |
paulb@738 | 244 | <input name="openid.claimed_id" type="hidden" value="%s" /> |
paulb@738 | 245 | <input name="openid.identity" type="hidden" value="%s" /> |
paulb@738 | 246 | <p>Please proceed to the OpenID provider: <input name="proceed" type="submit" value="Proceed!" /></p> |
paulb@738 | 247 | </form> |
paulb@733 | 248 | </body> |
paulb@733 | 249 | </html> |
paulb@733 | 250 | """ |
paulb@733 | 251 | |
paulb@733 | 252 | # General functions. |
paulb@733 | 253 | |
paulb@733 | 254 | def get_target(trans, urlencoding=None, encoding=None): |
paulb@733 | 255 | |
paulb@733 | 256 | """ |
paulb@733 | 257 | Return the application for 'trans' using the optional 'urlencoding' (or path |
paulb@733 | 258 | encoding) and request body 'encoding'. |
paulb@733 | 259 | """ |
paulb@733 | 260 | |
paulb@733 | 261 | fields_path = trans.get_fields_from_path(urlencoding) |
paulb@733 | 262 | fields_body = trans.get_fields_from_body(encoding) |
paulb@733 | 263 | |
paulb@733 | 264 | # NOTE: Handle missing redirects better. |
paulb@733 | 265 | |
paulb@733 | 266 | if fields_body.has_key("app"): |
paulb@733 | 267 | apps = fields_body["app"] |
paulb@733 | 268 | app = apps[0] |
paulb@733 | 269 | elif fields_path.has_key("app"): |
paulb@733 | 270 | apps = fields_path["app"] |
paulb@733 | 271 | app = apps[0] |
paulb@733 | 272 | else: |
paulb@733 | 273 | app = u"" |
paulb@733 | 274 | |
paulb@733 | 275 | return app |
paulb@733 | 276 | |
paulb@733 | 277 | # vim: tabstop=4 expandtab shiftwidth=4 |