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 26 from XSLForms.Resources.WebResources import XSLFormsResource 27 28 import WebStack.Resources.OpenIDRedirect # LoginRedirectResource 29 30 class OpenIDLoginResource(XSLFormsResource, OpenIDLoginUtils): 31 32 """ 33 A login screen resource which should be modified or subclassed to define the 34 following attributes: 35 36 * resource_dir 37 * template_resources - including a "login" entry for the login screen and 38 a "success" entry for a screen indicating a 39 successful login (used when redirects are not in 40 use) 41 * document_resources - including a "translations" entry 42 43 The latter attribute is optional. 44 45 The login template must define a "login" action, and provide a document 46 structure where the login credentials can be found through this class's 47 'path_to_login_element' attribute (which can be overridden or modified). 48 Such a structure would be as follows for the default configuration: 49 50 <login username="..." password="..."/> 51 52 The success template must provide a document structure where the location of 53 the application can be found through this class's 'path_to_success_element' 54 attribute (which can be overridden or modified). Such a structure would be 55 as follows for the default configuration: 56 57 <success location="..."/> 58 """ 59 60 path_to_login_element = "/login" 61 path_to_success_element = "/success" 62 63 def __init__(self, app_url, authenticator, associations=None, use_redirect=1): 64 65 """ 66 Initialise the resource with an 'app_url' and an 'authenticator'. 67 68 The optional 'associations' is a mapping from association handles to 69 secret keys. 70 71 If the optional 'use_redirect' flag is set to a false value (which is 72 not the default), a confirmation screen is given instead of immediately 73 redirecting the user back to the original application. 74 75 To get the root of the application, this resource needs an attribute on 76 the transaction called "root". 77 """ 78 79 OpenIDLoginUtils.__init__(self, associations, use_redirect) 80 self.app_url = app_url 81 self.authenticator = authenticator 82 83 def select_activity(self, trans, form): 84 form.set_activity("login") 85 86 def respond_to_input(self, trans, form): 87 parameters = form.get_parameters() 88 89 # Test for login. 90 91 if parameters.has_key("login"): 92 self.check_login(trans, form) 93 94 # Check for an OpenID signature verification request. 95 96 elif parameters.get("openid.mode", [None])[0] == "check_authentication": 97 self.check_authentication(trans, trans.get_fields()) 98 99 # NOTE: Permit association requests here. 100 # Otherwise, show the login form. 101 102 else: 103 self.show_login(trans, form) 104 105 # Methods called by the OpenID logic. 106 107 def check_login(self, trans, form): 108 doc = form.get_document() 109 parameters = form.get_parameters() 110 111 logelem = doc.xpath(self.path_to_login_element)[0] 112 return_to = logelem.getAttribute("return_to") or parameters.get("openid.return_to", [""])[0] 113 claimed_id = logelem.getAttribute("claimed_id") or parameters.get("openid.claimed_id", [""])[0] 114 local_id = logelem.getAttribute("identity") or parameters.get("openid.identity", [""])[0] 115 116 username = logelem.getAttribute("username") 117 password = logelem.getAttribute("password") 118 119 # If successful, switch to the success template and redirect. 120 # NOTE: Permit flexibility in the credentials. 121 122 if self.authenticator.authenticate(trans, (local_id, username), password): 123 endpoint = self.app_url + trans.get_path_without_query() 124 self.redirect_to_application(trans, form, claimed_id, local_id, username, return_to, endpoint) 125 else: 126 error = doc.createElement("error") 127 logelem.appendChild(error) 128 error.setAttribute("message", "Username or password not valid") 129 self.show_login(trans, form) 130 131 def redirect_to_application(self, trans, form, claimed_id, local_id, username, return_to, endpoint): 132 133 """ 134 Redirect the client using 'trans', 'claimed_id', 'local_id', 'username' 135 and the given 'return_to' and 'endpoint' details. 136 """ 137 138 fields = self.get_openid_fields(trans, claimed_id, local_id, username, return_to, endpoint) 139 url = self.get_openid_url(trans, fields) 140 141 # Show the success page anyway. 142 # Offer a POST-based form for redirection. 143 144 self.show_success(trans, form, fields) 145 if self.use_redirect: 146 trans.redirect(url) 147 148 def show_login(self, trans, form): 149 150 """ 151 Writes a login screen using the transaction 'trans' and 'form', 152 including details of the 'return_to' URL which the client was attempting 153 to access, along with the 'claimed_id' and 'local_id'. 154 """ 155 156 doc = form.get_document() 157 parameters = form.get_parameters() 158 159 logelem = doc.xpath(self.path_to_login_element)[0] 160 return_to = logelem.getAttribute("return_to") or parameters.get("openid.return_to", [""])[0] 161 claimed_id = logelem.getAttribute("claimed_id") or parameters.get("openid.claimed_id", [""])[0] 162 local_id = logelem.getAttribute("identity") or parameters.get("openid.identity", [""])[0] 163 164 logelem = doc.xpath(self.path_to_login_element)[0] 165 logelem.setAttribute("return_to", return_to) 166 logelem.setAttribute("claimed_id", claimed_id) 167 logelem.setAttribute("identity", local_id) 168 169 def show_success(self, trans, form, fields): 170 171 """ 172 Writes a success screen using the transaction 'trans' and 'form', using 173 a dictionary of 'fields' providing details of the transaction. 174 """ 175 176 # Switch to the success activity. 177 178 form.set_activity("success") 179 doc = form.new_instance("success") 180 successelem = doc.xpath(self.path_to_success_element)[0] 181 successelem.setAttribute("location", fields["openid.return_to"][0]) 182 183 # Add OpenID fields. 184 185 for name, values in fields.items(): 186 field = doc.createElement("field") 187 field.setAttribute("name", name) 188 field.setAttribute("value", values[0]) 189 successelem.appendChild(field) 190 191 form.set_document(doc) 192 193 # Output preparation. 194 195 def create_output(self, trans, form): 196 attributes = trans.get_attributes() 197 198 stylesheet_parameters = {} 199 references = {} 200 201 # Set up translations. 202 203 if self.document_resources.has_key("translations"): 204 translations_xml = self.prepare_document("translations") 205 206 try: 207 language = trans.get_content_languages()[0] 208 except IndexError: 209 language = "en" 210 211 stylesheet_parameters["locale"] = language 212 references["translations"] = translations_xml 213 214 # Complete the response. 215 216 stylesheet_parameters["root"] = attributes["root"] 217 XSLFormsResource.create_output(self, trans, form, stylesheet_parameters=stylesheet_parameters, references=references) 218 219 # vim: tabstop=4 expandtab shiftwidth=4