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