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