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