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 0, a confirmation screen 42 is given instead of redirecting the user back to the original 43 application. 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 105 self.show_success(trans, url) 106 107 # Redirect to the OpenID provider URL. 108 # NOTE: Offer a POST-based form for redirection. 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, url): 161 162 """ 163 Writes a success screen using the transaction 'trans', including details 164 of the OpenID provider 'url'. 165 """ 166 167 trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding)) 168 out = trans.get_response_stream() 169 out.write(self.success_page % (url, url)) 170 171 initiation_page = """ 172 <html> 173 <head> 174 <title>Authenticate via OpenID</title> 175 </head> 176 <body> 177 <h1>Authenticate via OpenID</h1> 178 <form method="POST" name="openid_identifier"> 179 <p>OpenID Identifier (URL): <input name="identity" type="text" size="32"/></p> 180 <p><input name="initiate" type="submit" value="Login"/></p> 181 <input name="app" type="hidden" value="%s"/> 182 </form> 183 </body> 184 </html> 185 """ 186 187 success_page = """ 188 <html> 189 <head> 190 <title>Authenticate via OpenID</title> 191 </head> 192 <body> 193 <h1>Authenticate via OpenID</h1> 194 <p>Please proceed to the OpenID provider: <a href="%s">%s</a>.</p> 195 </body> 196 </html> 197 """ 198 199 # General functions. 200 201 def get_target(trans, urlencoding=None, encoding=None): 202 203 """ 204 Return the application for 'trans' using the optional 'urlencoding' (or path 205 encoding) and request body 'encoding'. 206 """ 207 208 fields_path = trans.get_fields_from_path(urlencoding) 209 fields_body = trans.get_fields_from_body(encoding) 210 211 # NOTE: Handle missing redirects better. 212 213 if fields_body.has_key("app"): 214 apps = fields_body["app"] 215 app = apps[0] 216 elif fields_path.has_key("app"): 217 apps = fields_path["app"] 218 app = apps[0] 219 else: 220 app = u"" 221 222 return app 223 224 # vim: tabstop=4 expandtab shiftwidth=4