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 | |
paulb@756 | 79 | # NOTE: Not yet discovering XRI providers. |
paulb@756 | 80 | |
paulb@756 | 81 | if identity[0] in ("=", "@", "+", "$", "!", "("): |
paulb@756 | 82 | pass |
paulb@756 | 83 | else: |
paulb@756 | 84 | if not identity.startswith("http"): |
paulb@756 | 85 | identity = "http://" + identity |
paulb@756 | 86 | |
paulb@756 | 87 | # Obtain a provider url from a resource at the stated URL. |
paulb@756 | 88 | |
paulb@756 | 89 | doc = libxml2dom.parseURI(trans.encode_url_without_query(identity), html=1) |
paulb@756 | 90 | provider_links = doc.xpath("/html/head/link[contains(@rel, 'openid2.provider')]/@href") |
paulb@756 | 91 | local_ids = doc.xpath("/html/head/link[contains(@rel, 'openid2.local_id')]/@href") |
paulb@756 | 92 | if provider_links: |
paulb@756 | 93 | if local_ids: |
paulb@756 | 94 | return identity, provider_links[0].nodeValue, local_ids[0].nodeValue |
paulb@756 | 95 | else: |
paulb@756 | 96 | return identity, provider_links[0].nodeValue, None |
paulb@756 | 97 | |
paulb@756 | 98 | return identity, None, None |
paulb@756 | 99 | |
paulb@756 | 100 | class OpenIDInitiationResource(OpenIDInitiationUtils): |
paulb@733 | 101 | |
paulb@733 | 102 | "A resource providing an OpenID initiation screen." |
paulb@733 | 103 | |
paulb@742 | 104 | def __init__(self, openid_mode=None, use_redirect=1, urlencoding=None, encoding=None): |
paulb@733 | 105 | |
paulb@733 | 106 | """ |
paulb@733 | 107 | Initialise the resource. |
paulb@733 | 108 | |
paulb@733 | 109 | The optional 'openid_mode' parameter may be set to "checkid_immediate" |
paulb@733 | 110 | or "checkid_setup" (the default). |
paulb@733 | 111 | |
paulb@738 | 112 | If the optional 'use_redirect' flag is set to a false value (which is |
paulb@738 | 113 | not the default), a confirmation screen is given instead of immediately |
paulb@738 | 114 | redirecting the user to the OpenID provider. |
paulb@733 | 115 | |
paulb@733 | 116 | The optional 'urlencoding' parameter allows a special encoding to be |
paulb@733 | 117 | used in producing the redirection path. |
paulb@733 | 118 | |
paulb@733 | 119 | The optional 'encoding' parameter allows a special encoding to be used |
paulb@733 | 120 | in producing the initiation pages. |
paulb@733 | 121 | |
paulb@733 | 122 | To change the pages employed by this resource, either redefine the |
paulb@733 | 123 | 'initiation_page' and 'success_page' attributes in instances of this class or |
paulb@733 | 124 | a subclass, or override the 'show_initiation' and 'show_success' methods. |
paulb@733 | 125 | """ |
paulb@733 | 126 | |
paulb@756 | 127 | OpenIDInitiationUtils.__init__(self, openid_mode, use_redirect, urlencoding) |
paulb@752 | 128 | self.encoding = encoding |
paulb@733 | 129 | |
paulb@733 | 130 | def respond(self, trans): |
paulb@733 | 131 | |
paulb@733 | 132 | "Respond using the transaction 'trans'." |
paulb@733 | 133 | |
paulb@733 | 134 | app = get_target(trans, self.urlencoding, self.encoding) |
paulb@733 | 135 | |
paulb@733 | 136 | # Check for a submitted initiation form. |
paulb@733 | 137 | |
paulb@733 | 138 | fields_body = trans.get_fields_from_body(self.encoding) |
paulb@733 | 139 | |
paulb@733 | 140 | if fields_body.has_key("initiate") and fields_body.has_key("identity"): |
paulb@756 | 141 | self.check_identity(trans, fields_body, app) |
paulb@756 | 142 | # The above method does not return. |
paulb@733 | 143 | |
paulb@733 | 144 | # Otherwise, show the initiation form. |
paulb@733 | 145 | |
paulb@733 | 146 | self.show_initiation(trans, app) |
paulb@733 | 147 | |
paulb@756 | 148 | def check_identity(self, trans, fields, app): |
paulb@756 | 149 | |
paulb@756 | 150 | """ |
paulb@756 | 151 | Check the identity found through 'trans' and 'fields', using 'app' and |
paulb@756 | 152 | discovered information about the identity to redirect to the provider. |
paulb@756 | 153 | """ |
paulb@756 | 154 | |
paulb@756 | 155 | claimed_identifier, provider, local_identifier = self.get_provider_url(trans, fields["identity"][0]) |
paulb@756 | 156 | if provider is not None: |
paulb@756 | 157 | self.redirect_to_provider(trans, app, claimed_identifier, provider, local_identifier) |
paulb@756 | 158 | |
paulb@756 | 159 | def redirect_to_provider(self, trans, app, claimed_identifier, provider, local_identifier): |
paulb@733 | 160 | |
paulb@733 | 161 | """ |
paulb@733 | 162 | Redirect the client using 'trans' and the given 'app', |
paulb@733 | 163 | 'claimed_identifier', 'provider' and 'local_identifier' details. |
paulb@733 | 164 | |
paulb@733 | 165 | See: |
paulb@733 | 166 | http://openid.net/specs/openid-authentication-2_0-12.html#rfc.section.5.2 |
paulb@733 | 167 | http://openid.net/specs/openid-authentication-2_0-12.html#rfc.section.9 |
paulb@733 | 168 | """ |
paulb@733 | 169 | |
paulb@756 | 170 | url = self.get_redirect_url(trans, app, claimed_identifier, provider, local_identifier) |
paulb@733 | 171 | |
paulb@733 | 172 | # Show the success page anyway. |
paulb@738 | 173 | # Offer a POST-based form for redirection. |
paulb@733 | 174 | |
paulb@738 | 175 | self.show_success(trans, provider, app, claimed_identifier, local_identifier) |
paulb@733 | 176 | |
paulb@733 | 177 | # Redirect to the OpenID provider URL. |
paulb@733 | 178 | |
paulb@733 | 179 | if self.use_redirect: |
paulb@733 | 180 | trans.redirect(url) |
paulb@733 | 181 | else: |
paulb@733 | 182 | raise WebStack.Generic.EndOfResponse |
paulb@733 | 183 | |
paulb@733 | 184 | def show_initiation(self, trans, app): |
paulb@733 | 185 | |
paulb@733 | 186 | """ |
paulb@733 | 187 | Writes a initiation screen using the transaction 'trans', including details |
paulb@733 | 188 | of the 'app' which the client was attempting to access. |
paulb@733 | 189 | """ |
paulb@733 | 190 | |
paulb@756 | 191 | trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding or trans.default_charset)) |
paulb@733 | 192 | out = trans.get_response_stream() |
paulb@740 | 193 | out.write(self.initiation_page % cgi.escape(app)) |
paulb@733 | 194 | |
paulb@738 | 195 | def show_success(self, trans, provider, app, claimed_identifier, local_identifier): |
paulb@733 | 196 | |
paulb@733 | 197 | """ |
paulb@733 | 198 | Writes a success screen using the transaction 'trans', including details |
paulb@738 | 199 | of the OpenID 'provider', the 'app' URL, 'claimed_identifier' and |
paulb@738 | 200 | 'local_identifier'. |
paulb@733 | 201 | """ |
paulb@733 | 202 | |
paulb@756 | 203 | trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding or trans.default_charset)) |
paulb@733 | 204 | out = trans.get_response_stream() |
paulb@740 | 205 | out.write(self.success_page % tuple(map(cgi.escape, ( |
paulb@738 | 206 | provider, self.openid_ns, self.openid_mode, app, claimed_identifier, local_identifier) |
paulb@740 | 207 | ))) |
paulb@733 | 208 | |
paulb@733 | 209 | initiation_page = """ |
paulb@733 | 210 | <html> |
paulb@733 | 211 | <head> |
paulb@733 | 212 | <title>Authenticate via OpenID</title> |
paulb@733 | 213 | </head> |
paulb@733 | 214 | <body> |
paulb@733 | 215 | <h1>Authenticate via OpenID</h1> |
paulb@733 | 216 | <form method="POST" name="openid_identifier"> |
paulb@733 | 217 | <p>OpenID Identifier (URL): <input name="identity" type="text" size="32"/></p> |
paulb@738 | 218 | <p><input name="initiate" type="submit" value="Login" /></p> |
paulb@738 | 219 | <input name="app" type="hidden" value="%s" /> |
paulb@733 | 220 | </form> |
paulb@733 | 221 | </body> |
paulb@733 | 222 | </html> |
paulb@733 | 223 | """ |
paulb@733 | 224 | |
paulb@733 | 225 | success_page = """ |
paulb@733 | 226 | <html> |
paulb@733 | 227 | <head> |
paulb@733 | 228 | <title>Authenticate via OpenID</title> |
paulb@733 | 229 | </head> |
paulb@733 | 230 | <body> |
paulb@733 | 231 | <h1>Authenticate via OpenID</h1> |
paulb@738 | 232 | <form action="%s" method="POST" name="openid_redirect"> |
paulb@738 | 233 | <input name="openid.ns" type="hidden" value="%s" /> |
paulb@738 | 234 | <input name="openid.mode" type="hidden" value="%s" /> |
paulb@738 | 235 | <input name="openid.return_to" type="hidden" value="%s" /> |
paulb@738 | 236 | <input name="openid.claimed_id" type="hidden" value="%s" /> |
paulb@738 | 237 | <input name="openid.identity" type="hidden" value="%s" /> |
paulb@738 | 238 | <p>Please proceed to the OpenID provider: <input name="proceed" type="submit" value="Proceed!" /></p> |
paulb@738 | 239 | </form> |
paulb@733 | 240 | </body> |
paulb@733 | 241 | </html> |
paulb@733 | 242 | """ |
paulb@733 | 243 | |
paulb@733 | 244 | # General functions. |
paulb@733 | 245 | |
paulb@733 | 246 | def get_target(trans, urlencoding=None, encoding=None): |
paulb@733 | 247 | |
paulb@733 | 248 | """ |
paulb@733 | 249 | Return the application for 'trans' using the optional 'urlencoding' (or path |
paulb@733 | 250 | encoding) and request body 'encoding'. |
paulb@733 | 251 | """ |
paulb@733 | 252 | |
paulb@733 | 253 | fields_path = trans.get_fields_from_path(urlencoding) |
paulb@733 | 254 | fields_body = trans.get_fields_from_body(encoding) |
paulb@733 | 255 | |
paulb@733 | 256 | # NOTE: Handle missing redirects better. |
paulb@733 | 257 | |
paulb@733 | 258 | if fields_body.has_key("app"): |
paulb@733 | 259 | apps = fields_body["app"] |
paulb@733 | 260 | app = apps[0] |
paulb@733 | 261 | elif fields_path.has_key("app"): |
paulb@733 | 262 | apps = fields_path["app"] |
paulb@733 | 263 | app = apps[0] |
paulb@733 | 264 | else: |
paulb@733 | 265 | app = u"" |
paulb@733 | 266 | |
paulb@733 | 267 | return app |
paulb@733 | 268 | |
paulb@733 | 269 | # vim: tabstop=4 expandtab shiftwidth=4 |