1.1 --- a/WebStack/Resources/OpenIDInitiation.py Sun Feb 03 19:58:01 2008 +0000
1.2 +++ b/WebStack/Resources/OpenIDInitiation.py Sun Feb 03 20:00:03 2008 +0000
1.3 @@ -3,7 +3,7 @@
1.4 """
1.5 OpenID initiation resources which redirect clients to an OpenID provider.
1.6
1.7 -Copyright (C) 2004, 2005, 2006, 2007 Paul Boddie <paul@boddie.org.uk>
1.8 +Copyright (C) 2004, 2005, 2006, 2007, 2008 Paul Boddie <paul@boddie.org.uk>
1.9
1.10 This library is free software; you can redistribute it and/or
1.11 modify it under the terms of the GNU Lesser General Public
1.12 @@ -24,12 +24,83 @@
1.13 import libxml2dom
1.14 import cgi # for escape
1.15
1.16 -class OpenIDInitiationResource:
1.17 +class OpenIDInitiationUtils:
1.18 +
1.19 + "Utilities for OpenID initiation screens which may be inherited."
1.20 +
1.21 + openid_ns = "http://specs.openid.net/auth/2.0"
1.22 +
1.23 + def __init__(self, openid_mode=None, use_redirect=1, urlencoding=None):
1.24 +
1.25 + """
1.26 + Initialise the resource.
1.27 +
1.28 + The optional 'openid_mode' parameter may be set to "checkid_immediate"
1.29 + or "checkid_setup" (the default).
1.30 +
1.31 + If the optional 'use_redirect' flag is set to a false value (which is
1.32 + not the default), a confirmation screen is given instead of immediately
1.33 + redirecting the user to the OpenID provider.
1.34 +
1.35 + The optional 'urlencoding' parameter allows a special encoding to be
1.36 + used in producing the redirection path.
1.37 + """
1.38 +
1.39 + self.openid_mode = openid_mode or "checkid_setup"
1.40 + self.use_redirect = use_redirect
1.41 + self.urlencoding = urlencoding
1.42 +
1.43 + def get_redirect_url(self, trans, app, claimed_identifier, provider, local_identifier):
1.44 +
1.45 + # NOTE: Should consider the special "select" mode for identity.
1.46 +
1.47 + return "%s?openid.ns=%s&openid.mode=%s&openid.return_to=%s&openid.claimed_id=%s&openid.identity=%s" % (
1.48 + trans.encode_url_without_query(provider, self.urlencoding),
1.49 + trans.encode_path(self.openid_ns, self.urlencoding),
1.50 + trans.encode_path(self.openid_mode, self.urlencoding),
1.51 + trans.encode_path(app, self.urlencoding),
1.52 + trans.encode_path(claimed_identifier, self.urlencoding),
1.53 + trans.encode_path(local_identifier, self.urlencoding)
1.54 + )
1.55 +
1.56 + def get_provider_url(self, trans, identity):
1.57 +
1.58 + """
1.59 + Return the claimed identifier, provider URL and local identifier for the
1.60 + authenticating user using the given 'trans' and 'identity'.
1.61 +
1.62 + See:
1.63 + http://openid.net/specs/openid-authentication-2_0-12.html#rfc.section.7.3
1.64 + """
1.65 +
1.66 + if identity.startswith("xri://"):
1.67 + identity = openid[6:]
1.68 +
1.69 + # NOTE: Not yet discovering XRI providers.
1.70 +
1.71 + if identity[0] in ("=", "@", "+", "$", "!", "("):
1.72 + pass
1.73 + else:
1.74 + if not identity.startswith("http"):
1.75 + identity = "http://" + identity
1.76 +
1.77 + # Obtain a provider url from a resource at the stated URL.
1.78 +
1.79 + doc = libxml2dom.parseURI(trans.encode_url_without_query(identity), html=1)
1.80 + provider_links = doc.xpath("/html/head/link[contains(@rel, 'openid2.provider')]/@href")
1.81 + local_ids = doc.xpath("/html/head/link[contains(@rel, 'openid2.local_id')]/@href")
1.82 + if provider_links:
1.83 + if local_ids:
1.84 + return identity, provider_links[0].nodeValue, local_ids[0].nodeValue
1.85 + else:
1.86 + return identity, provider_links[0].nodeValue, None
1.87 +
1.88 + return identity, None, None
1.89 +
1.90 +class OpenIDInitiationResource(OpenIDInitiationUtils):
1.91
1.92 "A resource providing an OpenID initiation screen."
1.93
1.94 - openid_ns = "http://specs.openid.net/auth/2.0"
1.95 -
1.96 def __init__(self, openid_mode=None, use_redirect=1, urlencoding=None, encoding=None):
1.97
1.98 """
1.99 @@ -53,9 +124,7 @@
1.100 a subclass, or override the 'show_initiation' and 'show_success' methods.
1.101 """
1.102
1.103 - self.openid_mode = openid_mode or "checkid_setup"
1.104 - self.use_redirect = use_redirect
1.105 - self.urlencoding = urlencoding
1.106 + OpenIDInitiationUtils.__init__(self, openid_mode, use_redirect, urlencoding)
1.107 self.encoding = encoding
1.108
1.109 def respond(self, trans):
1.110 @@ -69,16 +138,25 @@
1.111 fields_body = trans.get_fields_from_body(self.encoding)
1.112
1.113 if fields_body.has_key("initiate") and fields_body.has_key("identity"):
1.114 - claimed_identifier, provider, local_identifier = self.get_provider_url(fields_body["identity"][0])
1.115 - if provider is not None:
1.116 - self._redirect(trans, app, claimed_identifier, provider, local_identifier)
1.117 - # The above method does not return.
1.118 + self.check_identity(trans, fields_body, app)
1.119 + # The above method does not return.
1.120
1.121 # Otherwise, show the initiation form.
1.122
1.123 self.show_initiation(trans, app)
1.124
1.125 - def _redirect(self, trans, app, claimed_identifier, provider, local_identifier):
1.126 + def check_identity(self, trans, fields, app):
1.127 +
1.128 + """
1.129 + Check the identity found through 'trans' and 'fields', using 'app' and
1.130 + discovered information about the identity to redirect to the provider.
1.131 + """
1.132 +
1.133 + claimed_identifier, provider, local_identifier = self.get_provider_url(trans, fields["identity"][0])
1.134 + if provider is not None:
1.135 + self.redirect_to_provider(trans, app, claimed_identifier, provider, local_identifier)
1.136 +
1.137 + def redirect_to_provider(self, trans, app, claimed_identifier, provider, local_identifier):
1.138
1.139 """
1.140 Redirect the client using 'trans' and the given 'app',
1.141 @@ -89,16 +167,7 @@
1.142 http://openid.net/specs/openid-authentication-2_0-12.html#rfc.section.9
1.143 """
1.144
1.145 - # NOTE: Should consider the special "select" mode for identity.
1.146 -
1.147 - url = "%s?openid.ns=%s&openid.mode=%s&openid.return_to=%s&openid.claimed_id=%s&openid.identity=%s" % (
1.148 - provider,
1.149 - trans.encode_path(self.openid_ns, self.urlencoding),
1.150 - trans.encode_path(self.openid_mode, self.urlencoding),
1.151 - trans.encode_path(app, self.urlencoding),
1.152 - trans.encode_path(claimed_identifier, self.urlencoding),
1.153 - trans.encode_path(local_identifier, self.urlencoding)
1.154 - )
1.155 + url = self.get_redirect_url(trans, app, claimed_identifier, provider, local_identifier)
1.156
1.157 # Show the success page anyway.
1.158 # Offer a POST-based form for redirection.
1.159 @@ -112,40 +181,6 @@
1.160 else:
1.161 raise WebStack.Generic.EndOfResponse
1.162
1.163 - def get_provider_url(self, identity):
1.164 -
1.165 - """
1.166 - Return the claimed identifier, provider URL and local identifier for the
1.167 - authenticating user using the given 'identity'.
1.168 -
1.169 - See:
1.170 - http://openid.net/specs/openid-authentication-2_0-12.html#rfc.section.7.3
1.171 - """
1.172 -
1.173 - if identity.startswith("xri://"):
1.174 - identity = openid[6:]
1.175 -
1.176 - # NOTE: Not yet discovering XRI providers.
1.177 -
1.178 - if identity[0] in ("=", "@", "+", "$", "!", "("):
1.179 - pass
1.180 - else:
1.181 - if not identity.startswith("http"):
1.182 - identity = "http://" + identity
1.183 -
1.184 - # Obtain a provider url from a resource at the stated URL.
1.185 -
1.186 - doc = libxml2dom.parseURI(identity, html=1)
1.187 - provider_links = doc.xpath("/html/head/link[contains(@rel, 'openid2.provider')]/@href")
1.188 - local_ids = doc.xpath("/html/head/link[contains(@rel, 'openid2.local_id')]/@href")
1.189 - if provider_links:
1.190 - if local_ids:
1.191 - return identity, provider_links[0].nodeValue, local_ids[0].nodeValue
1.192 - else:
1.193 - return identity, provider_links[0].nodeValue, None
1.194 -
1.195 - return identity, None, None
1.196 -
1.197 def show_initiation(self, trans, app):
1.198
1.199 """
1.200 @@ -153,7 +188,7 @@
1.201 of the 'app' which the client was attempting to access.
1.202 """
1.203
1.204 - trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding))
1.205 + trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding or trans.default_charset))
1.206 out = trans.get_response_stream()
1.207 out.write(self.initiation_page % cgi.escape(app))
1.208
1.209 @@ -165,7 +200,7 @@
1.210 'local_identifier'.
1.211 """
1.212
1.213 - trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding))
1.214 + trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding or trans.default_charset))
1.215 out = trans.get_response_stream()
1.216 out.write(self.success_page % tuple(map(cgi.escape, (
1.217 provider, self.openid_ns, self.openid_mode, app, claimed_identifier, local_identifier)
2.1 --- a/WebStack/Resources/OpenIDLogin.py Sun Feb 03 19:58:01 2008 +0000
2.2 +++ b/WebStack/Resources/OpenIDLogin.py Sun Feb 03 20:00:03 2008 +0000
2.3 @@ -4,7 +4,7 @@
2.4 OpenID provider login resources which redirect clients back to the application
2.5 ("relying party").
2.6
2.7 -Copyright (C) 2004, 2005, 2006, 2007 Paul Boddie <paul@boddie.org.uk>
2.8 +Copyright (C) 2004, 2005, 2006, 2007, 2008 Paul Boddie <paul@boddie.org.uk>
2.9
2.10 This library is free software; you can redistribute it and/or
2.11 modify it under the terms of the GNU Lesser General Public
2.12 @@ -27,6 +27,7 @@
2.13 import time
2.14 import random
2.15 import cgi # for escape
2.16 +import urlparse # for urlsplit
2.17
2.18 class OpenIDLoginUtils:
2.19
2.20 @@ -35,15 +36,28 @@
2.21 openid_ns = "http://specs.openid.net/auth/2.0"
2.22 signed_names = ["op_endpoint", "return_to", "response_nonce", "assoc_handle", "claimed_id", "identity"]
2.23
2.24 - def __init__(self, associations=None, use_redirect=1):
2.25 + def __init__(self, app_url, authenticator, associations=None, use_redirect=1, urlencoding=None):
2.26 +
2.27 + """
2.28 + Initialise the resource with the application URL 'app_url' and an
2.29 + 'authenticator'.
2.30 +
2.31 + The optional 'associations' is a mapping from association handles to
2.32 + secret keys.
2.33 +
2.34 + If the optional 'use_redirect' flag is set to a false value (which is
2.35 + not the default), a confirmation screen is given instead of immediately
2.36 + redirecting the user back to the original application.
2.37 +
2.38 + The optional 'urlencoding' parameter allows a special encoding to be
2.39 + used in producing the redirection path.
2.40 + """
2.41 +
2.42 + self.app_url = app_url
2.43 + self.authenticator = authenticator
2.44 self.associations = associations or {}
2.45 self.use_redirect = use_redirect
2.46 -
2.47 - def urlencode(self, trans, value):
2.48 - if not trans.default_charset:
2.49 - return trans.encode_path(value, self.urlencoding)
2.50 - else:
2.51 - return trans.encode_path(value)
2.52 + self.urlencoding = urlencoding
2.53
2.54 def get_openid_fields(self, trans, claimed_id, local_id, username, return_to, endpoint):
2.55
2.56 @@ -81,13 +95,13 @@
2.57
2.58 # Build an URL for returning to the application.
2.59
2.60 - url = "%s?" % fields["openid.return_to"][0]
2.61 + url = trans.encode_url_without_query(fields["openid.return_to"][0]) + "?"
2.62
2.63 first = 1
2.64 for name, value in fields.items():
2.65 if not first:
2.66 url += "&"
2.67 - url += "%s=%s" % (name, self.urlencode(trans, value[0]))
2.68 + url += "%s=%s" % (name, trans.encode_path(value[0]))
2.69 first = 0
2.70
2.71 return url
2.72 @@ -147,26 +161,6 @@
2.73
2.74 self.show_verification(trans, valid)
2.75
2.76 - def check_login(self, trans, fields):
2.77 -
2.78 - "Check the login details supplied in 'trans' and 'fields'."
2.79 -
2.80 - return_to = fields.get("openid.return_to", [""])[0]
2.81 - claimed_id = fields.get("openid.claimed_id", [""])[0]
2.82 - local_id = fields.get("openid.identity", [""])[0]
2.83 -
2.84 - # Check a combination of local identifier and username together with
2.85 - # the password.
2.86 -
2.87 - username = fields.get("username", [""])[0]
2.88 - password = fields.get("password", [""])[0]
2.89 -
2.90 - # NOTE: Permit flexibility in the credentials.
2.91 -
2.92 - if self.authenticator.authenticate(trans, (local_id, username), password):
2.93 - endpoint = self.app_url + trans.get_path_without_query(self.urlencoding)
2.94 - self.redirect_to_application(trans, claimed_id, local_id, username, return_to, endpoint)
2.95 -
2.96 class OpenIDLoginResource(OpenIDLoginUtils):
2.97
2.98 "A resource providing a login screen."
2.99 @@ -195,10 +189,7 @@
2.100 a subclass, or override the 'show_login' and 'show_success' methods.
2.101 """
2.102
2.103 - OpenIDLoginUtils.__init__(self, associations, use_redirect)
2.104 - self.app_url = app_url
2.105 - self.authenticator = authenticator
2.106 - self.urlencoding = urlencoding
2.107 + OpenIDLoginUtils.__init__(self, app_url, authenticator, associations, use_redirect, urlencoding)
2.108 self.encoding = encoding
2.109
2.110 def respond(self, trans):
2.111 @@ -224,6 +215,26 @@
2.112
2.113 self.show_login(trans, fields)
2.114
2.115 + def check_login(self, trans, fields):
2.116 +
2.117 + "Check the login details supplied in 'trans' and 'fields'."
2.118 +
2.119 + return_to = fields.get("openid.return_to", [""])[0]
2.120 + claimed_id = fields.get("openid.claimed_id", [""])[0]
2.121 + local_id = fields.get("openid.identity", [""])[0]
2.122 +
2.123 + # Check a combination of local identifier and username together with
2.124 + # the password.
2.125 +
2.126 + username = fields.get("username", [""])[0]
2.127 + password = fields.get("password", [""])[0]
2.128 +
2.129 + # NOTE: Permit flexibility in the credentials.
2.130 +
2.131 + if self.authenticator.authenticate(trans, (local_id, username), password):
2.132 + endpoint = self.app_url + trans.get_path_without_query(self.urlencoding)
2.133 + self.redirect_to_application(trans, claimed_id, local_id, username, return_to, endpoint)
2.134 +
2.135 def show_login(self, trans, fields):
2.136
2.137 """
2.138 @@ -234,7 +245,7 @@
2.139 claimed_id = fields.get("openid.claimed_id", [""])[0]
2.140 local_id = fields.get("openid.identity", [""])[0]
2.141
2.142 - trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding))
2.143 + trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding or trans.default_charset))
2.144 out = trans.get_response_stream()
2.145 out.write(self.login_page % tuple(map(cgi.escape, (return_to, claimed_id, local_id))))
2.146
2.147 @@ -245,7 +256,7 @@
2.148 dictionary of 'fields' providing details of the transaction.
2.149 """
2.150
2.151 - trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding))
2.152 + trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding or trans.default_charset))
2.153 out = trans.get_response_stream()
2.154 l = []
2.155 for name, values in fields.items():
3.1 --- a/WebStack/Resources/OpenIDRedirect.py Sun Feb 03 19:58:01 2008 +0000
3.2 +++ b/WebStack/Resources/OpenIDRedirect.py Sun Feb 03 20:00:03 2008 +0000
3.3 @@ -49,7 +49,7 @@
3.4 # fields.get("openid.ns", [None])[0] == self.openid_ns
3.5
3.6 if self.authenticator.authenticate(trans, verify=1):
3.7 - trans.redirect(fields["openid.return_to"][0])
3.8 + trans.redirect(trans.encode_url_without_query(fields["openid.return_to"][0]))
3.9
3.10 # Otherwise, handle the usual parameters and request details.
3.11
3.12 @@ -102,7 +102,7 @@
3.13 # Test the details of the assertion.
3.14
3.15 if self.test_url(fields) and \
3.16 - self.test_signature(fields) and \
3.17 + self.test_signature(trans, fields) and \
3.18 self.test_replay(fields):
3.19
3.20 self.set_token(trans, fields["openid.identity"][0])
3.21 @@ -140,7 +140,7 @@
3.22
3.23 return fields["openid.return_to"][0].startswith(self.app_url)
3.24
3.25 - def test_signature(self, fields):
3.26 + def test_signature(self, trans, fields):
3.27
3.28 """
3.29 See:
3.30 @@ -164,13 +164,13 @@
3.31 # from the OpenID provider.
3.32
3.33 else:
3.34 - return self.test_signature_direct(fields)
3.35 + return self.test_signature_direct(trans, fields)
3.36
3.37 # Without a handle, no signature verification can occur.
3.38
3.39 return 0
3.40
3.41 - def test_signature_direct(self, fields):
3.42 + def test_signature_direct(self, trans, fields):
3.43
3.44 """
3.45 See:
3.46 @@ -179,12 +179,12 @@
3.47
3.48 # Make a POST request using the "openid." fields.
3.49
3.50 - d = {}
3.51 + d = []
3.52 for name, values in fields.items():
3.53 if name.startswith("openid.") and name != "openid.mode":
3.54 - d[name] = values[0]
3.55 - d["openid.mode"] = "check_authentication"
3.56 - data = urllib.urlencode(d)
3.57 + d.append("%s=%s" % (name, trans.encode_path(values[0])))
3.58 + d.append("%s=%s" % ("openid.mode", "check_authentication"))
3.59 + data = "&".join(d)
3.60
3.61 # Send a POST request to the OpenID provider, reading the response and
3.62 # testing for certain fields and values.