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