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@733 | 6 | Copyright (C) 2004, 2005, 2006, 2007 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@733 | 27 | class OpenIDInitiationResource: |
paulb@733 | 28 | |
paulb@733 | 29 | "A resource providing an OpenID initiation screen." |
paulb@733 | 30 | |
paulb@733 | 31 | encoding = "utf-8" |
paulb@733 | 32 | openid_ns = "http://specs.openid.net/auth/2.0" |
paulb@733 | 33 | |
paulb@742 | 34 | def __init__(self, openid_mode=None, use_redirect=1, urlencoding=None, encoding=None): |
paulb@733 | 35 | |
paulb@733 | 36 | """ |
paulb@733 | 37 | Initialise the resource. |
paulb@733 | 38 | |
paulb@733 | 39 | The optional 'openid_mode' parameter may be set to "checkid_immediate" |
paulb@733 | 40 | or "checkid_setup" (the default). |
paulb@733 | 41 | |
paulb@738 | 42 | If the optional 'use_redirect' flag is set to a false value (which is |
paulb@738 | 43 | not the default), a confirmation screen is given instead of immediately |
paulb@738 | 44 | redirecting the user to the OpenID provider. |
paulb@733 | 45 | |
paulb@733 | 46 | The optional 'urlencoding' parameter allows a special encoding to be |
paulb@733 | 47 | used in producing the redirection path. |
paulb@733 | 48 | |
paulb@733 | 49 | The optional 'encoding' parameter allows a special encoding to be used |
paulb@733 | 50 | in producing the initiation pages. |
paulb@733 | 51 | |
paulb@733 | 52 | To change the pages employed by this resource, either redefine the |
paulb@733 | 53 | 'initiation_page' and 'success_page' attributes in instances of this class or |
paulb@733 | 54 | a subclass, or override the 'show_initiation' and 'show_success' methods. |
paulb@733 | 55 | """ |
paulb@733 | 56 | |
paulb@733 | 57 | self.openid_mode = openid_mode or "checkid_setup" |
paulb@733 | 58 | self.use_redirect = use_redirect |
paulb@733 | 59 | self.urlencoding = urlencoding |
paulb@733 | 60 | self.encoding = encoding or self.encoding |
paulb@733 | 61 | |
paulb@733 | 62 | def respond(self, trans): |
paulb@733 | 63 | |
paulb@733 | 64 | "Respond using the transaction 'trans'." |
paulb@733 | 65 | |
paulb@733 | 66 | app = get_target(trans, self.urlencoding, self.encoding) |
paulb@733 | 67 | |
paulb@733 | 68 | # Check for a submitted initiation form. |
paulb@733 | 69 | |
paulb@733 | 70 | fields_body = trans.get_fields_from_body(self.encoding) |
paulb@733 | 71 | |
paulb@733 | 72 | if fields_body.has_key("initiate") and fields_body.has_key("identity"): |
paulb@733 | 73 | claimed_identifier, provider, local_identifier = self.get_provider_url(fields_body["identity"][0]) |
paulb@733 | 74 | if provider is not None: |
paulb@733 | 75 | self._redirect(trans, app, claimed_identifier, provider, local_identifier) |
paulb@733 | 76 | # The above method does not return. |
paulb@733 | 77 | |
paulb@733 | 78 | # Otherwise, show the initiation form. |
paulb@733 | 79 | |
paulb@733 | 80 | self.show_initiation(trans, app) |
paulb@733 | 81 | |
paulb@733 | 82 | def _redirect(self, trans, app, claimed_identifier, provider, local_identifier): |
paulb@733 | 83 | |
paulb@733 | 84 | """ |
paulb@733 | 85 | Redirect the client using 'trans' and the given 'app', |
paulb@733 | 86 | 'claimed_identifier', 'provider' and 'local_identifier' details. |
paulb@733 | 87 | |
paulb@733 | 88 | See: |
paulb@733 | 89 | http://openid.net/specs/openid-authentication-2_0-12.html#rfc.section.5.2 |
paulb@733 | 90 | http://openid.net/specs/openid-authentication-2_0-12.html#rfc.section.9 |
paulb@733 | 91 | """ |
paulb@733 | 92 | |
paulb@733 | 93 | # NOTE: Should consider the special "select" mode for identity. |
paulb@733 | 94 | |
paulb@733 | 95 | url = "%s?openid.ns=%s&openid.mode=%s&openid.return_to=%s&openid.claimed_id=%s&openid.identity=%s" % ( |
paulb@733 | 96 | provider, |
paulb@733 | 97 | trans.encode_path(self.openid_ns, self.urlencoding), |
paulb@733 | 98 | trans.encode_path(self.openid_mode, self.urlencoding), |
paulb@733 | 99 | trans.encode_path(app, self.urlencoding), |
paulb@733 | 100 | trans.encode_path(claimed_identifier, self.urlencoding), |
paulb@733 | 101 | trans.encode_path(local_identifier, self.urlencoding) |
paulb@733 | 102 | ) |
paulb@733 | 103 | |
paulb@733 | 104 | # Show the success page anyway. |
paulb@738 | 105 | # Offer a POST-based form for redirection. |
paulb@733 | 106 | |
paulb@738 | 107 | self.show_success(trans, provider, app, claimed_identifier, local_identifier) |
paulb@733 | 108 | |
paulb@733 | 109 | # Redirect to the OpenID provider URL. |
paulb@733 | 110 | |
paulb@733 | 111 | if self.use_redirect: |
paulb@733 | 112 | trans.redirect(url) |
paulb@733 | 113 | else: |
paulb@733 | 114 | raise WebStack.Generic.EndOfResponse |
paulb@733 | 115 | |
paulb@733 | 116 | def get_provider_url(self, identity): |
paulb@733 | 117 | |
paulb@733 | 118 | """ |
paulb@733 | 119 | Return the claimed identifier, provider URL and local identifier for the |
paulb@733 | 120 | authenticating user using the given 'identity'. |
paulb@733 | 121 | |
paulb@733 | 122 | See: |
paulb@733 | 123 | http://openid.net/specs/openid-authentication-2_0-12.html#rfc.section.7.3 |
paulb@733 | 124 | """ |
paulb@733 | 125 | |
paulb@733 | 126 | if identity.startswith("xri://"): |
paulb@733 | 127 | identity = openid[6:] |
paulb@733 | 128 | |
paulb@733 | 129 | # NOTE: Not yet discovering XRI providers. |
paulb@733 | 130 | |
paulb@733 | 131 | if identity[0] in ("=", "@", "+", "$", "!", "("): |
paulb@733 | 132 | pass |
paulb@733 | 133 | else: |
paulb@733 | 134 | if not identity.startswith("http"): |
paulb@733 | 135 | identity = "http://" + identity |
paulb@733 | 136 | |
paulb@733 | 137 | # Obtain a provider url from a resource at the stated URL. |
paulb@733 | 138 | |
paulb@733 | 139 | doc = libxml2dom.parseURI(identity, html=1) |
paulb@733 | 140 | provider_links = doc.xpath("/html/head/link[contains(@rel, 'openid2.provider')]/@href") |
paulb@733 | 141 | local_ids = doc.xpath("/html/head/link[contains(@rel, 'openid2.local_id')]/@href") |
paulb@733 | 142 | if provider_links: |
paulb@733 | 143 | if local_ids: |
paulb@733 | 144 | return identity, provider_links[0].nodeValue, local_ids[0].nodeValue |
paulb@733 | 145 | else: |
paulb@733 | 146 | return identity, provider_links[0].nodeValue, None |
paulb@733 | 147 | |
paulb@733 | 148 | return identity, None, None |
paulb@733 | 149 | |
paulb@733 | 150 | def show_initiation(self, trans, app): |
paulb@733 | 151 | |
paulb@733 | 152 | """ |
paulb@733 | 153 | Writes a initiation screen using the transaction 'trans', including details |
paulb@733 | 154 | of the 'app' which the client was attempting to access. |
paulb@733 | 155 | """ |
paulb@733 | 156 | |
paulb@733 | 157 | trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding)) |
paulb@733 | 158 | out = trans.get_response_stream() |
paulb@740 | 159 | out.write(self.initiation_page % cgi.escape(app)) |
paulb@733 | 160 | |
paulb@738 | 161 | def show_success(self, trans, provider, app, claimed_identifier, local_identifier): |
paulb@733 | 162 | |
paulb@733 | 163 | """ |
paulb@733 | 164 | Writes a success screen using the transaction 'trans', including details |
paulb@738 | 165 | of the OpenID 'provider', the 'app' URL, 'claimed_identifier' and |
paulb@738 | 166 | 'local_identifier'. |
paulb@733 | 167 | """ |
paulb@733 | 168 | |
paulb@733 | 169 | trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding)) |
paulb@733 | 170 | out = trans.get_response_stream() |
paulb@740 | 171 | out.write(self.success_page % tuple(map(cgi.escape, ( |
paulb@738 | 172 | provider, self.openid_ns, self.openid_mode, app, claimed_identifier, local_identifier) |
paulb@740 | 173 | ))) |
paulb@733 | 174 | |
paulb@733 | 175 | initiation_page = """ |
paulb@733 | 176 | <html> |
paulb@733 | 177 | <head> |
paulb@733 | 178 | <title>Authenticate via OpenID</title> |
paulb@733 | 179 | </head> |
paulb@733 | 180 | <body> |
paulb@733 | 181 | <h1>Authenticate via OpenID</h1> |
paulb@733 | 182 | <form method="POST" name="openid_identifier"> |
paulb@733 | 183 | <p>OpenID Identifier (URL): <input name="identity" type="text" size="32"/></p> |
paulb@738 | 184 | <p><input name="initiate" type="submit" value="Login" /></p> |
paulb@738 | 185 | <input name="app" type="hidden" value="%s" /> |
paulb@733 | 186 | </form> |
paulb@733 | 187 | </body> |
paulb@733 | 188 | </html> |
paulb@733 | 189 | """ |
paulb@733 | 190 | |
paulb@733 | 191 | success_page = """ |
paulb@733 | 192 | <html> |
paulb@733 | 193 | <head> |
paulb@733 | 194 | <title>Authenticate via OpenID</title> |
paulb@733 | 195 | </head> |
paulb@733 | 196 | <body> |
paulb@733 | 197 | <h1>Authenticate via OpenID</h1> |
paulb@738 | 198 | <form action="%s" method="POST" name="openid_redirect"> |
paulb@738 | 199 | <input name="openid.ns" type="hidden" value="%s" /> |
paulb@738 | 200 | <input name="openid.mode" type="hidden" value="%s" /> |
paulb@738 | 201 | <input name="openid.return_to" type="hidden" value="%s" /> |
paulb@738 | 202 | <input name="openid.claimed_id" type="hidden" value="%s" /> |
paulb@738 | 203 | <input name="openid.identity" type="hidden" value="%s" /> |
paulb@738 | 204 | <p>Please proceed to the OpenID provider: <input name="proceed" type="submit" value="Proceed!" /></p> |
paulb@738 | 205 | </form> |
paulb@733 | 206 | </body> |
paulb@733 | 207 | </html> |
paulb@733 | 208 | """ |
paulb@733 | 209 | |
paulb@733 | 210 | # General functions. |
paulb@733 | 211 | |
paulb@733 | 212 | def get_target(trans, urlencoding=None, encoding=None): |
paulb@733 | 213 | |
paulb@733 | 214 | """ |
paulb@733 | 215 | Return the application for 'trans' using the optional 'urlencoding' (or path |
paulb@733 | 216 | encoding) and request body 'encoding'. |
paulb@733 | 217 | """ |
paulb@733 | 218 | |
paulb@733 | 219 | fields_path = trans.get_fields_from_path(urlencoding) |
paulb@733 | 220 | fields_body = trans.get_fields_from_body(encoding) |
paulb@733 | 221 | |
paulb@733 | 222 | # NOTE: Handle missing redirects better. |
paulb@733 | 223 | |
paulb@733 | 224 | if fields_body.has_key("app"): |
paulb@733 | 225 | apps = fields_body["app"] |
paulb@733 | 226 | app = apps[0] |
paulb@733 | 227 | elif fields_path.has_key("app"): |
paulb@733 | 228 | apps = fields_path["app"] |
paulb@733 | 229 | app = apps[0] |
paulb@733 | 230 | else: |
paulb@733 | 231 | app = u"" |
paulb@733 | 232 | |
paulb@733 | 233 | return app |
paulb@733 | 234 | |
paulb@733 | 235 | # vim: tabstop=4 expandtab shiftwidth=4 |