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 109 if self.use_redirect: 110 trans.redirect(url) 111 else: 112 raise WebStack.Generic.EndOfResponse 113 114 def get_provider_url(self, identity): 115 116 """ 117 Return the claimed identifier, provider URL and local identifier for the 118 authenticating user using the given 'identity'. 119 120 See: 121 http://openid.net/specs/openid-authentication-2_0-12.html#rfc.section.7.3 122 """ 123 124 if identity.startswith("xri://"): 125 identity = openid[6:] 126 127 # NOTE: Not yet discovering XRI providers. 128 129 if identity[0] in ("=", "@", "+", "$", "!", "("): 130 pass 131 else: 132 if not identity.startswith("http"): 133 identity = "http://" + identity 134 135 # Obtain a provider url from a resource at the stated URL. 136 137 doc = libxml2dom.parseURI(identity, html=1) 138 provider_links = doc.xpath("/html/head/link[contains(@rel, 'openid2.provider')]/@href") 139 local_ids = doc.xpath("/html/head/link[contains(@rel, 'openid2.local_id')]/@href") 140 if provider_links: 141 if local_ids: 142 return identity, provider_links[0].nodeValue, local_ids[0].nodeValue 143 else: 144 return identity, provider_links[0].nodeValue, None 145 146 return identity, None, None 147 148 def show_initiation(self, trans, app): 149 150 """ 151 Writes a initiation screen using the transaction 'trans', including details 152 of the 'app' which the client was attempting to access. 153 """ 154 155 trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding)) 156 out = trans.get_response_stream() 157 out.write(self.initiation_page % app) 158 159 def show_success(self, trans, url): 160 161 """ 162 Writes a success screen using the transaction 'trans', including details 163 of the OpenID provider 'url'. 164 """ 165 166 trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding)) 167 out = trans.get_response_stream() 168 out.write(self.success_page % (url, url)) 169 170 initiation_page = """ 171 <html> 172 <head> 173 <title>Authenticate via OpenID</title> 174 </head> 175 <body> 176 <h1>Authenticate via OpenID</h1> 177 <form method="POST" name="openid_identifier"> 178 <p>OpenID Identifier (URL): <input name="identity" type="text" size="32"/></p> 179 <p><input name="initiate" type="submit" value="Login"/></p> 180 <input name="app" type="hidden" value="%s"/> 181 </form> 182 </body> 183 </html> 184 """ 185 186 success_page = """ 187 <html> 188 <head> 189 <title>Authenticate via OpenID</title> 190 </head> 191 <body> 192 <h1>Authenticate via OpenID</h1> 193 <p>Please proceed to the OpenID provider: <a href="%s">%s</a>.</p> 194 </body> 195 </html> 196 """ 197 198 # General functions. 199 200 def get_target(trans, urlencoding=None, encoding=None): 201 202 """ 203 Return the application for 'trans' using the optional 'urlencoding' (or path 204 encoding) and request body 'encoding'. 205 """ 206 207 fields_path = trans.get_fields_from_path(urlencoding) 208 fields_body = trans.get_fields_from_body(encoding) 209 210 # NOTE: Handle missing redirects better. 211 212 if fields_body.has_key("app"): 213 apps = fields_body["app"] 214 app = apps[0] 215 elif fields_path.has_key("app"): 216 apps = fields_path["app"] 217 app = apps[0] 218 else: 219 app = u"" 220 221 return app 222 223 # vim: tabstop=4 expandtab shiftwidth=4