1 #!/usr/bin/env python 2 3 """ 4 OpenID login resources for XSLForms applications. These resources use "root" 5 attributes on transaction objects, and therefore should be defined within the 6 appropriate resources in site maps. 7 8 Copyright (C) 2006, 2007 Paul Boddie <paul@boddie.org.uk> 9 10 This program is free software; you can redistribute it and/or modify it under 11 the terms of the GNU Lesser General Public License as published by the Free 12 Software Foundation; either version 3 of the License, or (at your option) any 13 later version. 14 15 This program is distributed in the hope that it will be useful, but WITHOUT 16 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 17 FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 18 details. 19 20 You should have received a copy of the GNU Lesser General Public License along 21 with this program. If not, see <http://www.gnu.org/licenses/>. 22 """ 23 24 from WebStack.Generic import ContentType, EndOfResponse 25 from WebStack.Resources.OpenIDLogin import OpenIDLoginUtils, Authenticator 26 from XSLForms.Resources.WebResources import XSLFormsResource 27 28 class OpenIDLoginResource(XSLFormsResource, OpenIDLoginUtils): 29 30 """ 31 A login screen resource which should be modified or subclassed to define the 32 following attributes: 33 34 * resource_dir 35 * template_resources - including a "login" entry for the login screen and 36 a "success" entry for a screen indicating a 37 successful login (used when redirects are not in 38 use) 39 * document_resources - including a "translations" entry 40 41 The latter attribute is optional. 42 43 The login template must define a "login" action, and provide a document 44 structure where the login credentials can be found through this class's 45 'path_to_login_element' attribute (which can be overridden or modified). 46 Such a structure would be as follows for the default configuration: 47 48 <login username="..." password="..."/> 49 50 The success template must provide a document structure where the location of 51 the application can be found through this class's 'path_to_success_element' 52 attribute (which can be overridden or modified). Such a structure would be 53 as follows for the default configuration: 54 55 <success location="..."/> 56 """ 57 58 path_to_login_element = "/login" 59 path_to_success_element = "/success" 60 61 def __init__(self, app_url, authenticator, associations=None, use_redirect=1): 62 63 """ 64 Initialise the resource with an 'app_url' and an 'authenticator'. 65 66 The optional 'associations' is a mapping from association handles to 67 secret keys. 68 69 If the optional 'use_redirect' flag is set to a false value (which is 70 not the default), a confirmation screen is given instead of immediately 71 redirecting the user back to the original application. 72 73 To get the root of the application, this resource needs an attribute on 74 the transaction called "root". 75 """ 76 77 OpenIDLoginUtils.__init__(self, app_url, authenticator, associations, use_redirect) 78 79 def select_activity(self, trans, form): 80 form.set_activity("login") 81 82 def respond_to_input(self, trans, form): 83 parameters = form.get_parameters() 84 85 # Test for login. 86 87 if parameters.has_key("login"): 88 self.check_login(trans, form) 89 90 # Check for an OpenID signature verification request. 91 92 elif parameters.get("openid.mode", [None])[0] == "check_authentication": 93 self.check_authentication(trans, trans.get_fields()) 94 95 # NOTE: Permit association requests here. 96 # Otherwise, show the login form. 97 98 else: 99 self.show_login(trans, form) 100 101 # Methods called by the OpenID logic. 102 103 def check_login(self, trans, form): 104 doc = form.get_document() 105 parameters = form.get_parameters() 106 107 logelem = doc.xpath(self.path_to_login_element)[0] 108 return_to = logelem.getAttribute("return_to") or parameters.get("openid.return_to", [""])[0] 109 claimed_id = logelem.getAttribute("claimed_id") or parameters.get("openid.claimed_id", [""])[0] 110 local_id = logelem.getAttribute("identity") or parameters.get("openid.identity", [""])[0] 111 112 username = logelem.getAttribute("username") 113 password = logelem.getAttribute("password") 114 115 # If successful, switch to the success template and redirect. 116 # NOTE: Permit flexibility in the credentials. 117 118 if self.authenticator.authenticate(trans, (local_id, username), password): 119 endpoint = self.app_url + trans.get_path_without_query() 120 self.redirect_to_application(trans, form, claimed_id, local_id, username, return_to, endpoint) 121 else: 122 error = doc.createElement("error") 123 logelem.appendChild(error) 124 error.setAttribute("message", "Username or password not valid") 125 self.show_login(trans, form) 126 127 def redirect_to_application(self, trans, form, claimed_id, local_id, username, return_to, endpoint): 128 129 """ 130 Redirect the client using 'trans', 'claimed_id', 'local_id', 'username' 131 and the given 'return_to' and 'endpoint' details. 132 """ 133 134 fields = self.get_openid_fields(trans, claimed_id, local_id, username, return_to, endpoint) 135 url = self.get_openid_url(trans, fields) 136 137 # Show the success page anyway. 138 # Offer a POST-based form for redirection. 139 140 self.show_success(trans, form, fields) 141 if self.use_redirect: 142 trans.redirect(url) 143 144 def show_login(self, trans, form): 145 146 """ 147 Writes a login screen using the transaction 'trans' and 'form', 148 including details of the 'return_to' URL which the client was attempting 149 to access, along with the 'claimed_id' and 'local_id'. 150 """ 151 152 doc = form.get_document() 153 parameters = form.get_parameters() 154 155 logelem = doc.xpath(self.path_to_login_element)[0] 156 return_to = logelem.getAttribute("return_to") or parameters.get("openid.return_to", [""])[0] 157 claimed_id = logelem.getAttribute("claimed_id") or parameters.get("openid.claimed_id", [""])[0] 158 local_id = logelem.getAttribute("identity") or parameters.get("openid.identity", [""])[0] 159 160 logelem = doc.xpath(self.path_to_login_element)[0] 161 logelem.setAttribute("return_to", return_to) 162 logelem.setAttribute("claimed_id", claimed_id) 163 logelem.setAttribute("identity", local_id) 164 165 def show_success(self, trans, form, fields): 166 167 """ 168 Writes a success screen using the transaction 'trans' and 'form', using 169 a dictionary of 'fields' providing details of the transaction. 170 """ 171 172 # Switch to the success activity. 173 174 form.set_activity("success") 175 doc = form.new_instance("success") 176 successelem = doc.xpath(self.path_to_success_element)[0] 177 successelem.setAttribute("location", fields["openid.return_to"][0]) 178 179 # Add OpenID fields. 180 181 for name, values in fields.items(): 182 field = doc.createElement("field") 183 field.setAttribute("name", name) 184 field.setAttribute("value", values[0]) 185 successelem.appendChild(field) 186 187 form.set_document(doc) 188 189 # Output preparation. 190 191 def create_output(self, trans, form): 192 attributes = trans.get_attributes() 193 194 stylesheet_parameters = {} 195 references = {} 196 197 # Set up translations. 198 199 if self.document_resources.has_key("translations"): 200 translations_xml = self.prepare_document("translations") 201 202 try: 203 language = trans.get_content_languages()[0] 204 except IndexError: 205 language = "en" 206 207 stylesheet_parameters["locale"] = language 208 references["translations"] = translations_xml 209 210 # Complete the response. 211 212 stylesheet_parameters["root"] = attributes["root"] 213 XSLFormsResource.create_output(self, trans, form, stylesheet_parameters=stylesheet_parameters, references=references) 214 215 # vim: tabstop=4 expandtab shiftwidth=4