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