summaryrefslogtreecommitdiff
path: root/paste/auth
diff options
context:
space:
mode:
Diffstat (limited to 'paste/auth')
-rw-r--r--paste/auth/__init__.py9
-rw-r--r--paste/auth/auth_tkt.py429
-rw-r--r--paste/auth/basic.py122
-rw-r--r--paste/auth/cas.py99
-rw-r--r--paste/auth/cookie.py405
-rw-r--r--paste/auth/digest.py254
-rw-r--r--paste/auth/form.py149
-rw-r--r--paste/auth/grantip.py114
-rw-r--r--paste/auth/multi.py79
-rw-r--r--paste/auth/open_id.py413
10 files changed, 2073 insertions, 0 deletions
diff --git a/paste/auth/__init__.py b/paste/auth/__init__.py
new file mode 100644
index 0000000..186e2ef
--- /dev/null
+++ b/paste/auth/__init__.py
@@ -0,0 +1,9 @@
+# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
+# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
+"""
+Package for authentication/identification of requests.
+
+The objective of this package is to provide single-focused middleware
+components that implement a particular specification. Integration of
+the components into a usable system is up to a higher-level framework.
+"""
diff --git a/paste/auth/auth_tkt.py b/paste/auth/auth_tkt.py
new file mode 100644
index 0000000..da8ddbd
--- /dev/null
+++ b/paste/auth/auth_tkt.py
@@ -0,0 +1,429 @@
+# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
+# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
+##########################################################################
+#
+# Copyright (c) 2005 Imaginary Landscape LLC and Contributors.
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+##########################################################################
+"""
+Implementation of cookie signing as done in `mod_auth_tkt
+<http://www.openfusion.com.au/labs/mod_auth_tkt/>`_.
+
+mod_auth_tkt is an Apache module that looks for these signed cookies
+and sets ``REMOTE_USER``, ``REMOTE_USER_TOKENS`` (a comma-separated
+list of groups) and ``REMOTE_USER_DATA`` (arbitrary string data).
+
+This module is an alternative to the ``paste.auth.cookie`` module;
+it's primary benefit is compatibility with mod_auth_tkt, which in turn
+makes it possible to use the same authentication process with
+non-Python code run under Apache.
+"""
+
+import time as time_mod
+try:
+ import hashlib
+except ImportError:
+ # mimic hashlib (will work for md5, fail for secure hashes)
+ import md5 as hashlib
+try:
+ from http.cookies import SimpleCookie
+except ImportError:
+ # Python 2
+ from Cookie import SimpleCookie
+from paste import request
+from urllib import quote as url_quote
+from urllib import unquote as url_unquote
+
+DEFAULT_DIGEST = hashlib.md5
+
+
+class AuthTicket(object):
+
+ """
+ This class represents an authentication token. You must pass in
+ the shared secret, the userid, and the IP address. Optionally you
+ can include tokens (a list of strings, representing role names),
+ 'user_data', which is arbitrary data available for your own use in
+ later scripts. Lastly, you can override the timestamp, cookie name,
+ whether to secure the cookie and the digest algorithm (for details
+ look at ``AuthTKTMiddleware``).
+
+ Once you provide all the arguments, use .cookie_value() to
+ generate the appropriate authentication ticket. .cookie()
+ generates a Cookie object, the str() of which is the complete
+ cookie header to be sent.
+
+ CGI usage::
+
+ token = auth_tkt.AuthTick('sharedsecret', 'username',
+ os.environ['REMOTE_ADDR'], tokens=['admin'])
+ print('Status: 200 OK')
+ print('Content-type: text/html')
+ print(token.cookie())
+ print("")
+ ... redirect HTML ...
+
+ Webware usage::
+
+ token = auth_tkt.AuthTick('sharedsecret', 'username',
+ self.request().environ()['REMOTE_ADDR'], tokens=['admin'])
+ self.response().setCookie('auth_tkt', token.cookie_value())
+
+ Be careful not to do an HTTP redirect after login; use meta
+ refresh or Javascript -- some browsers have bugs where cookies
+ aren't saved when set on a redirect.
+ """
+
+ def __init__(self, secret, userid, ip, tokens=(), user_data='',
+ time=None, cookie_name='auth_tkt',
+ secure=False, digest_algo=DEFAULT_DIGEST):
+ self.secret = secret
+ self.userid = userid
+ self.ip = ip
+ if not isinstance(tokens, basestring):
+ tokens = ','.join(tokens)
+ self.tokens = tokens
+ self.user_data = user_data
+ if time is None:
+ self.time = time_mod.time()
+ else:
+ self.time = time
+ self.cookie_name = cookie_name
+ self.secure = secure
+ if isinstance(digest_algo, str):
+ # correct specification of digest from hashlib or fail
+ self.digest_algo = getattr(hashlib, digest_algo)
+ else:
+ self.digest_algo = digest_algo
+
+ def digest(self):
+ return calculate_digest(
+ self.ip, self.time, self.secret, self.userid, self.tokens,
+ self.user_data, self.digest_algo)
+
+ def cookie_value(self):
+ v = '%s%08x%s!' % (self.digest(), int(self.time), url_quote(self.userid))
+ if self.tokens:
+ v += self.tokens + '!'
+ v += self.user_data
+ return v
+
+ def cookie(self):
+ c = SimpleCookie()
+ c[self.cookie_name] = self.cookie_value().encode('base64').strip().replace('\n', '')
+ c[self.cookie_name]['path'] = '/'
+ if self.secure:
+ c[self.cookie_name]['secure'] = 'true'
+ return c
+
+
+class BadTicket(Exception):
+ """
+ Exception raised when a ticket can't be parsed. If we get
+ far enough to determine what the expected digest should have
+ been, expected is set. This should not be shown by default,
+ but can be useful for debugging.
+ """
+ def __init__(self, msg, expected=None):
+ self.expected = expected
+ Exception.__init__(self, msg)
+
+
+def parse_ticket(secret, ticket, ip, digest_algo=DEFAULT_DIGEST):
+ """
+ Parse the ticket, returning (timestamp, userid, tokens, user_data).
+
+ If the ticket cannot be parsed, ``BadTicket`` will be raised with
+ an explanation.
+ """
+ if isinstance(digest_algo, str):
+ # correct specification of digest from hashlib or fail
+ digest_algo = getattr(hashlib, digest_algo)
+ digest_hexa_size = digest_algo().digest_size * 2
+ ticket = ticket.strip('"')
+ digest = ticket[:digest_hexa_size]
+ try:
+ timestamp = int(ticket[digest_hexa_size:digest_hexa_size + 8], 16)
+ except ValueError as e:
+ raise BadTicket('Timestamp is not a hex integer: %s' % e)
+ try:
+ userid, data = ticket[digest_hexa_size + 8:].split('!', 1)
+ except ValueError:
+ raise BadTicket('userid is not followed by !')
+ userid = url_unquote(userid)
+ if '!' in data:
+ tokens, user_data = data.split('!', 1)
+ else:
+ # @@: Is this the right order?
+ tokens = ''
+ user_data = data
+
+ expected = calculate_digest(ip, timestamp, secret,
+ userid, tokens, user_data,
+ digest_algo)
+
+ if expected != digest:
+ raise BadTicket('Digest signature is not correct',
+ expected=(expected, digest))
+
+ tokens = tokens.split(',')
+
+ return (timestamp, userid, tokens, user_data)
+
+
+# @@: Digest object constructor compatible with named ones in hashlib only
+def calculate_digest(ip, timestamp, secret, userid, tokens, user_data,
+ digest_algo):
+ secret = maybe_encode(secret)
+ userid = maybe_encode(userid)
+ tokens = maybe_encode(tokens)
+ user_data = maybe_encode(user_data)
+ digest0 = digest_algo(
+ encode_ip_timestamp(ip, timestamp) + secret + userid + '\0'
+ + tokens + '\0' + user_data).hexdigest()
+ digest = digest_algo(digest0 + secret).hexdigest()
+ return digest
+
+
+def encode_ip_timestamp(ip, timestamp):
+ ip_chars = ''.join(map(chr, map(int, ip.split('.'))))
+ t = int(timestamp)
+ ts = ((t & 0xff000000) >> 24,
+ (t & 0xff0000) >> 16,
+ (t & 0xff00) >> 8,
+ t & 0xff)
+ ts_chars = ''.join(map(chr, ts))
+ return ip_chars + ts_chars
+
+
+def maybe_encode(s, encoding='utf8'):
+ if isinstance(s, unicode):
+ s = s.encode(encoding)
+ return s
+
+
+class AuthTKTMiddleware(object):
+
+ """
+ Middleware that checks for signed cookies that match what
+ `mod_auth_tkt <http://www.openfusion.com.au/labs/mod_auth_tkt/>`_
+ looks for (if you have mod_auth_tkt installed, you don't need this
+ middleware, since Apache will set the environmental variables for
+ you).
+
+ Arguments:
+
+ ``secret``:
+ A secret that should be shared by any instances of this application.
+ If this app is served from more than one machine, they should all
+ have the same secret.
+
+ ``cookie_name``:
+ The name of the cookie to read and write from. Default ``auth_tkt``.
+
+ ``secure``:
+ If the cookie should be set as 'secure' (only sent over SSL) and if
+ the login must be over SSL. (Defaults to False)
+
+ ``httponly``:
+ If the cookie should be marked as HttpOnly, which means that it's
+ not accessible to JavaScript. (Defaults to False)
+
+ ``include_ip``:
+ If the cookie should include the user's IP address. If so, then
+ if they change IPs their cookie will be invalid.
+
+ ``logout_path``:
+ The path under this middleware that should signify a logout. The
+ page will be shown as usual, but the user will also be logged out
+ when they visit this page.
+
+ ``digest_algo``:
+ Digest algorithm specified as a name of the algorithm provided by
+ ``hashlib`` or as a compatible digest object constructor.
+ Defaults to ``md5``, as in mod_auth_tkt. The others currently
+ compatible with mod_auth_tkt are ``sha256`` and ``sha512``.
+
+ If used with mod_auth_tkt, then these settings (except logout_path) should
+ match the analogous Apache configuration settings.
+
+ This also adds two functions to the request:
+
+ ``environ['paste.auth_tkt.set_user'](userid, tokens='', user_data='')``
+
+ This sets a cookie that logs the user in. ``tokens`` is a
+ string (comma-separated groups) or a list of strings.
+ ``user_data`` is a string for your own use.
+
+ ``environ['paste.auth_tkt.logout_user']()``
+
+ Logs out the user.
+ """
+
+ def __init__(self, app, secret, cookie_name='auth_tkt', secure=False,
+ include_ip=True, logout_path=None, httponly=False,
+ no_domain_cookie=True, current_domain_cookie=True,
+ wildcard_cookie=True, digest_algo=DEFAULT_DIGEST):
+ self.app = app
+ self.secret = secret
+ self.cookie_name = cookie_name
+ self.secure = secure
+ self.httponly = httponly
+ self.include_ip = include_ip
+ self.logout_path = logout_path
+ self.no_domain_cookie = no_domain_cookie
+ self.current_domain_cookie = current_domain_cookie
+ self.wildcard_cookie = wildcard_cookie
+ if isinstance(digest_algo, str):
+ # correct specification of digest from hashlib or fail
+ self.digest_algo = getattr(hashlib, digest_algo)
+ else:
+ self.digest_algo = digest_algo
+
+ def __call__(self, environ, start_response):
+ cookies = request.get_cookies(environ)
+ if self.cookie_name in cookies:
+ cookie_value = cookies[self.cookie_name].value
+ else:
+ cookie_value = ''
+ if cookie_value:
+ if self.include_ip:
+ remote_addr = environ['REMOTE_ADDR']
+ else:
+ # mod_auth_tkt uses this dummy value when IP is not
+ # checked:
+ remote_addr = '0.0.0.0'
+ # @@: This should handle bad signatures better:
+ # Also, timeouts should cause cookie refresh
+ try:
+ timestamp, userid, tokens, user_data = parse_ticket(
+ self.secret, cookie_value, remote_addr, self.digest_algo)
+ tokens = ','.join(tokens)
+ environ['REMOTE_USER'] = userid
+ if environ.get('REMOTE_USER_TOKENS'):
+ # We want to add tokens/roles to what's there:
+ tokens = environ['REMOTE_USER_TOKENS'] + ',' + tokens
+ environ['REMOTE_USER_TOKENS'] = tokens
+ environ['REMOTE_USER_DATA'] = user_data
+ environ['AUTH_TYPE'] = 'cookie'
+ except BadTicket:
+ # bad credentials, just ignore without logging the user
+ # in or anything
+ pass
+ set_cookies = []
+
+ def set_user(userid, tokens='', user_data=''):
+ set_cookies.extend(self.set_user_cookie(
+ environ, userid, tokens, user_data))
+
+ def logout_user():
+ set_cookies.extend(self.logout_user_cookie(environ))
+
+ environ['paste.auth_tkt.set_user'] = set_user
+ environ['paste.auth_tkt.logout_user'] = logout_user
+ if self.logout_path and environ.get('PATH_INFO') == self.logout_path:
+ logout_user()
+
+ def cookie_setting_start_response(status, headers, exc_info=None):
+ headers.extend(set_cookies)
+ return start_response(status, headers, exc_info)
+
+ return self.app(environ, cookie_setting_start_response)
+
+ def set_user_cookie(self, environ, userid, tokens, user_data):
+ if not isinstance(tokens, basestring):
+ tokens = ','.join(tokens)
+ if self.include_ip:
+ remote_addr = environ['REMOTE_ADDR']
+ else:
+ remote_addr = '0.0.0.0'
+ ticket = AuthTicket(
+ self.secret,
+ userid,
+ remote_addr,
+ tokens=tokens,
+ user_data=user_data,
+ cookie_name=self.cookie_name,
+ secure=self.secure)
+ # @@: Should we set REMOTE_USER etc in the current
+ # environment right now as well?
+ cur_domain = environ.get('HTTP_HOST', environ.get('SERVER_NAME'))
+ wild_domain = '.' + cur_domain
+
+ cookie_options = ""
+ if self.secure:
+ cookie_options += "; secure"
+ if self.httponly:
+ cookie_options += "; HttpOnly"
+
+ cookies = []
+ if self.no_domain_cookie:
+ cookies.append(('Set-Cookie', '%s=%s; Path=/%s' % (
+ self.cookie_name, ticket.cookie_value(), cookie_options)))
+ if self.current_domain_cookie:
+ cookies.append(('Set-Cookie', '%s=%s; Path=/; Domain=%s%s' % (
+ self.cookie_name, ticket.cookie_value(), cur_domain,
+ cookie_options)))
+ if self.wildcard_cookie:
+ cookies.append(('Set-Cookie', '%s=%s; Path=/; Domain=%s%s' % (
+ self.cookie_name, ticket.cookie_value(), wild_domain,
+ cookie_options)))
+
+ return cookies
+
+ def logout_user_cookie(self, environ):
+ cur_domain = environ.get('HTTP_HOST', environ.get('SERVER_NAME'))
+ wild_domain = '.' + cur_domain
+ expires = 'Sat, 01-Jan-2000 12:00:00 GMT'
+ cookies = [
+ ('Set-Cookie', '%s=""; Expires="%s"; Path=/' % (self.cookie_name, expires)),
+ ('Set-Cookie', '%s=""; Expires="%s"; Path=/; Domain=%s' %
+ (self.cookie_name, expires, cur_domain)),
+ ('Set-Cookie', '%s=""; Expires="%s"; Path=/; Domain=%s' %
+ (self.cookie_name, expires, wild_domain)),
+ ]
+ return cookies
+
+
+def make_auth_tkt_middleware(
+ app,
+ global_conf,
+ secret=None,
+ cookie_name='auth_tkt',
+ secure=False,
+ include_ip=True,
+ logout_path=None):
+ """
+ Creates the `AuthTKTMiddleware
+ <class-paste.auth.auth_tkt.AuthTKTMiddleware.html>`_.
+
+ ``secret`` is requird, but can be set globally or locally.
+ """
+ from paste.deploy.converters import asbool
+ secure = asbool(secure)
+ include_ip = asbool(include_ip)
+ if secret is None:
+ secret = global_conf.get('secret')
+ if not secret:
+ raise ValueError(
+ "You must provide a 'secret' (in global or local configuration)")
+ return AuthTKTMiddleware(
+ app, secret, cookie_name, secure, include_ip, logout_path or None)
diff --git a/paste/auth/basic.py b/paste/auth/basic.py
new file mode 100644
index 0000000..24d1731
--- /dev/null
+++ b/paste/auth/basic.py
@@ -0,0 +1,122 @@
+# (c) 2005 Clark C. Evans
+# This module is part of the Python Paste Project and is released under
+# the MIT License: http://www.opensource.org/licenses/mit-license.php
+# This code was written with funding by http://prometheusresearch.com
+"""
+Basic HTTP/1.0 Authentication
+
+This module implements ``Basic`` authentication as described in
+HTTP/1.0 specification [1]_ . Do not use this module unless you
+are using SSL or need to work with very out-dated clients, instead
+use ``digest`` authentication.
+
+>>> from paste.wsgilib import dump_environ
+>>> from paste.httpserver import serve
+>>> # from paste.auth.basic import AuthBasicHandler
+>>> realm = 'Test Realm'
+>>> def authfunc(environ, username, password):
+... return username == password
+>>> serve(AuthBasicHandler(dump_environ, realm, authfunc))
+serving on...
+
+.. [1] http://www.w3.org/Protocols/HTTP/1.0/draft-ietf-http-spec.html#BasicAA
+"""
+from paste.httpexceptions import HTTPUnauthorized
+from paste.httpheaders import *
+
+class AuthBasicAuthenticator(object):
+ """
+ implements ``Basic`` authentication details
+ """
+ type = 'basic'
+ def __init__(self, realm, authfunc):
+ self.realm = realm
+ self.authfunc = authfunc
+
+ def build_authentication(self):
+ head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
+ return HTTPUnauthorized(headers=head)
+
+ def authenticate(self, environ):
+ authorization = AUTHORIZATION(environ)
+ if not authorization:
+ return self.build_authentication()
+ (authmeth, auth) = authorization.split(' ', 1)
+ if 'basic' != authmeth.lower():
+ return self.build_authentication()
+ auth = auth.strip().decode('base64')
+ username, password = auth.split(':', 1)
+ if self.authfunc(environ, username, password):
+ return username
+ return self.build_authentication()
+
+ __call__ = authenticate
+
+class AuthBasicHandler(object):
+ """
+ HTTP/1.0 ``Basic`` authentication middleware
+
+ Parameters:
+
+ ``application``
+
+ The application object is called only upon successful
+ authentication, and can assume ``environ['REMOTE_USER']``
+ is set. If the ``REMOTE_USER`` is already set, this
+ middleware is simply pass-through.
+
+ ``realm``
+
+ This is a identifier for the authority that is requesting
+ authorization. It is shown to the user and should be unique
+ within the domain it is being used.
+
+ ``authfunc``
+
+ This is a mandatory user-defined function which takes a
+ ``environ``, ``username`` and ``password`` for its first
+ three arguments. It should return ``True`` if the user is
+ authenticated.
+
+ """
+ def __init__(self, application, realm, authfunc):
+ self.application = application
+ self.authenticate = AuthBasicAuthenticator(realm, authfunc)
+
+ def __call__(self, environ, start_response):
+ username = REMOTE_USER(environ)
+ if not username:
+ result = self.authenticate(environ)
+ if isinstance(result, str):
+ AUTH_TYPE.update(environ, 'basic')
+ REMOTE_USER.update(environ, result)
+ else:
+ return result.wsgi_application(environ, start_response)
+ return self.application(environ, start_response)
+
+middleware = AuthBasicHandler
+
+__all__ = ['AuthBasicHandler']
+
+def make_basic(app, global_conf, realm, authfunc, **kw):
+ """
+ Grant access via basic authentication
+
+ Config looks like this::
+
+ [filter:grant]
+ use = egg:Paste#auth_basic
+ realm=myrealm
+ authfunc=somepackage.somemodule:somefunction
+
+ """
+ from paste.util.import_string import eval_import
+ import types
+ authfunc = eval_import(authfunc)
+ assert isinstance(authfunc, types.FunctionType), "authfunc must resolve to a function"
+ return AuthBasicHandler(app, realm, authfunc)
+
+
+if "__main__" == __name__:
+ import doctest
+ doctest.testmod(optionflags=doctest.ELLIPSIS)
diff --git a/paste/auth/cas.py b/paste/auth/cas.py
new file mode 100644
index 0000000..44e4e98
--- /dev/null
+++ b/paste/auth/cas.py
@@ -0,0 +1,99 @@
+# (c) 2005 Clark C. Evans
+# This module is part of the Python Paste Project and is released under
+# the MIT License: http://www.opensource.org/licenses/mit-license.php
+# This code was written with funding by http://prometheusresearch.com
+"""
+CAS 1.0 Authentication
+
+The Central Authentication System is a straight-forward single sign-on
+mechanism developed by Yale University's ITS department. It has since
+enjoyed widespread success and is deployed at many major universities
+and some corporations.
+
+ https://clearinghouse.ja-sig.org/wiki/display/CAS/Home
+ http://www.yale.edu/tp/auth/usingcasatyale.html
+
+This implementation has the goal of maintaining current path arguments
+passed to the system so that it can be used as middleware at any stage
+of processing. It has the secondary goal of allowing for other
+authentication methods to be used concurrently.
+"""
+from six.moves.urllib.parse import urlencode
+from paste.request import construct_url
+from paste.httpexceptions import HTTPSeeOther, HTTPForbidden
+
+class CASLoginFailure(HTTPForbidden):
+ """ The exception raised if the authority returns 'no' """
+
+class CASAuthenticate(HTTPSeeOther):
+ """ The exception raised to authenticate the user """
+
+def AuthCASHandler(application, authority):
+ """
+ middleware to implement CAS 1.0 authentication
+
+ There are several possible outcomes:
+
+ 0. If the REMOTE_USER environment variable is already populated;
+ then this middleware is a no-op, and the request is passed along
+ to the application.
+
+ 1. If a query argument 'ticket' is found, then an attempt to
+ validate said ticket /w the authentication service done. If the
+ ticket is not validated; an 403 'Forbidden' exception is raised.
+ Otherwise, the REMOTE_USER variable is set with the NetID that
+ was validated and AUTH_TYPE is set to "cas".
+
+ 2. Otherwise, a 303 'See Other' is returned to the client directing
+ them to login using the CAS service. After logon, the service
+ will send them back to this same URL, only with a 'ticket' query
+ argument.
+
+ Parameters:
+
+ ``authority``
+
+ This is a fully-qualified URL to a CAS 1.0 service. The URL
+ should end with a '/' and have the 'login' and 'validate'
+ sub-paths as described in the CAS 1.0 documentation.
+
+ """
+ assert authority.endswith("/") and authority.startswith("http")
+ def cas_application(environ, start_response):
+ username = environ.get('REMOTE_USER','')
+ if username:
+ return application(environ, start_response)
+ qs = environ.get('QUERY_STRING','').split("&")
+ if qs and qs[-1].startswith("ticket="):
+ # assume a response from the authority
+ ticket = qs.pop().split("=", 1)[1]
+ environ['QUERY_STRING'] = "&".join(qs)
+ service = construct_url(environ)
+ args = urlencode(
+ {'service': service,'ticket': ticket})
+ requrl = authority + "validate?" + args
+ result = urlopen(requrl).read().split("\n")
+ if 'yes' == result[0]:
+ environ['REMOTE_USER'] = result[1]
+ environ['AUTH_TYPE'] = 'cas'
+ return application(environ, start_response)
+ exce = CASLoginFailure()
+ else:
+ service = construct_url(environ)
+ args = urlencode({'service': service})
+ location = authority + "login?" + args
+ exce = CASAuthenticate(location)
+ return exce.wsgi_application(environ, start_response)
+ return cas_application
+
+middleware = AuthCASHandler
+
+__all__ = ['CASLoginFailure', 'CASAuthenticate', 'AuthCASHandler' ]
+
+if '__main__' == __name__:
+ authority = "https://secure.its.yale.edu/cas/servlet/"
+ from paste.wsgilib import dump_environ
+ from paste.httpserver import serve
+ from paste.httpexceptions import *
+ serve(HTTPExceptionHandler(
+ AuthCASHandler(dump_environ, authority)))
diff --git a/paste/auth/cookie.py b/paste/auth/cookie.py
new file mode 100644
index 0000000..8f11d1b
--- /dev/null
+++ b/paste/auth/cookie.py
@@ -0,0 +1,405 @@
+# (c) 2005 Clark C. Evans
+# This module is part of the Python Paste Project and is released under
+# the MIT License: http://www.opensource.org/licenses/mit-license.php
+# This code was written with funding by http://prometheusresearch.com
+"""
+Cookie "Saved" Authentication
+
+This authentication middleware saves the current REMOTE_USER,
+REMOTE_SESSION, and any other environment variables specified in a
+cookie so that it can be retrieved during the next request without
+requiring re-authentication. This uses a session cookie on the client
+side (so it goes away when the user closes their window) and does
+server-side expiration.
+
+Following is a very simple example where a form is presented asking for
+a user name (no actual checking), and dummy session identifier (perhaps
+corresponding to a database session id) is stored in the cookie.
+
+::
+
+ >>> from paste.httpserver import serve
+ >>> from paste.fileapp import DataApp
+ >>> from paste.httpexceptions import *
+ >>> from paste.auth.cookie import AuthCookieHandler
+ >>> from paste.wsgilib import parse_querystring
+ >>> def testapp(environ, start_response):
+ ... user = dict(parse_querystring(environ)).get('user','')
+ ... if user:
+ ... environ['REMOTE_USER'] = user
+ ... environ['REMOTE_SESSION'] = 'a-session-id'
+ ... if environ.get('REMOTE_USER'):
+ ... page = '<html><body>Welcome %s (%s)</body></html>'
+ ... page %= (environ['REMOTE_USER'], environ['REMOTE_SESSION'])
+ ... else:
+ ... page = ('<html><body><form><input name="user" />'
+ ... '<input type="submit" /></form></body></html>')
+ ... return DataApp(page, content_type="text/html")(
+ ... environ, start_response)
+ >>> serve(AuthCookieHandler(testapp))
+ serving on...
+
+"""
+
+import hmac, base64, random, six, time, warnings
+try:
+ from hashlib import sha1
+except ImportError:
+ # NOTE: We have to use the callable with hashlib (hashlib.sha1),
+ # otherwise hmac only accepts the sha module object itself
+ import sha as sha1
+from paste.request import get_cookies
+
+def make_time(value):
+ return time.strftime("%Y%m%d%H%M", time.gmtime(value))
+_signature_size = len(hmac.new(b'x', b'x', sha1).digest())
+_header_size = _signature_size + len(make_time(time.time()))
+
+# @@: Should this be using urllib.quote?
+# build encode/decode functions to safely pack away values
+_encode = [('\\', '\\x5c'), ('"', '\\x22'),
+ ('=', '\\x3d'), (';', '\\x3b')]
+_decode = [(v, k) for (k, v) in _encode]
+_decode.reverse()
+def encode(s, sublist = _encode):
+ return six.moves.reduce((lambda a, b: a.replace(b[0], b[1])), sublist, str(s))
+decode = lambda s: encode(s, _decode)
+
+class CookieTooLarge(RuntimeError):
+ def __init__(self, content, cookie):
+ RuntimeError.__init__("Signed cookie exceeds maximum size of 4096")
+ self.content = content
+ self.cookie = cookie
+
+_all_chars = ''.join([chr(x) for x in range(0, 255)])
+def new_secret():
+ """ returns a 64 byte secret """
+ secret = ''.join(random.sample(_all_chars, 64))
+ if six.PY3:
+ secret = secret.encode('utf8')
+ return secret
+
+class AuthCookieSigner(object):
+ """
+ save/restore ``environ`` entries via digially signed cookie
+
+ This class converts content into a timed and digitally signed
+ cookie, as well as having the facility to reverse this procedure.
+ If the cookie, after the content is encoded and signed exceeds the
+ maximum length (4096), then CookieTooLarge exception is raised.
+
+ The timeout of the cookie is handled on the server side for a few
+ reasons. First, if a 'Expires' directive is added to a cookie, then
+ the cookie becomes persistent (lasting even after the browser window
+ has closed). Second, the user's clock may be wrong (perhaps
+ intentionally). The timeout is specified in minutes; and expiration
+ date returned is rounded to one second.
+
+ Constructor Arguments:
+
+ ``secret``
+
+ This is a secret key if you want to syncronize your keys so
+ that the cookie will be good across a cluster of computers.
+ It is recommended via the HMAC specification (RFC 2104) that
+ the secret key be 64 bytes since this is the block size of
+ the hashing. If you do not provide a secret key, a random
+ one is generated each time you create the handler; this
+ should be sufficient for most cases.
+
+ ``timeout``
+
+ This is the time (in minutes) from which the cookie is set
+ to expire. Note that on each request a new (replacement)
+ cookie is sent, hence this is effectively a session timeout
+ parameter for your entire cluster. If you do not provide a
+ timeout, it is set at 30 minutes.
+
+ ``maxlen``
+
+ This is the maximum size of the *signed* cookie; hence the
+ actual content signed will be somewhat less. If the cookie
+ goes over this size, a ``CookieTooLarge`` exception is
+ raised so that unexpected handling of cookies on the client
+ side are avoided. By default this is set at 4k (4096 bytes),
+ which is the standard cookie size limit.
+
+ """
+ def __init__(self, secret = None, timeout = None, maxlen = None):
+ self.timeout = timeout or 30
+ if isinstance(timeout, six.string_types):
+ raise ValueError(
+ "Timeout must be a number (minutes), not a string (%r)"
+ % timeout)
+ self.maxlen = maxlen or 4096
+ self.secret = secret or new_secret()
+
+ def sign(self, content):
+ """
+ Sign the content returning a valid cookie (that does not
+ need to be escaped and quoted). The expiration of this
+ cookie is handled server-side in the auth() function.
+ """
+ timestamp = make_time(time.time() + 60*self.timeout)
+ if six.PY3:
+ content = content.encode('utf8')
+ timestamp = timestamp.encode('utf8')
+ cookie = base64.encodestring(
+ hmac.new(self.secret, content, sha1).digest() +
+ timestamp +
+ content)
+ cookie = cookie.replace(b"/", b"_").replace(b"=", b"~")
+ cookie = cookie.replace(b'\n', b'').replace(b'\r', b'')
+ if len(cookie) > self.maxlen:
+ raise CookieTooLarge(content, cookie)
+ return cookie
+
+ def auth(self, cookie):
+ """
+ Authenticate the cooke using the signature, verify that it
+ has not expired; and return the cookie's content
+ """
+ decode = base64.decodestring(
+ cookie.replace("_", "/").replace("~", "="))
+ signature = decode[:_signature_size]
+ expires = decode[_signature_size:_header_size]
+ content = decode[_header_size:]
+ if signature == hmac.new(self.secret, content, sha1).digest():
+ if int(expires) > int(make_time(time.time())):
+ return content
+ else:
+ # This is the normal case of an expired cookie; just
+ # don't bother doing anything here.
+ pass
+ else:
+ # This case can happen if the server is restarted with a
+ # different secret; or if the user's IP address changed
+ # due to a proxy. However, it could also be a break-in
+ # attempt -- so should it be reported?
+ pass
+
+class AuthCookieEnviron(list):
+ """
+ a list of environment keys to be saved via cookie
+
+ An instance of this object, found at ``environ['paste.auth.cookie']``
+ lists the `environ` keys that were restored from or will be added
+ to the digially signed cookie. This object can be accessed from an
+ `environ` variable by using this module's name.
+ """
+ def __init__(self, handler, scanlist):
+ list.__init__(self, scanlist)
+ self.handler = handler
+ def append(self, value):
+ if value in self:
+ return
+ list.append(self, str(value))
+
+class AuthCookieHandler(object):
+ """
+ the actual handler that should be put in your middleware stack
+
+ This middleware uses cookies to stash-away a previously authenticated
+ user (and perhaps other variables) so that re-authentication is not
+ needed. This does not implement sessions; and therefore N servers
+ can be syncronized to accept the same saved authentication if they
+ all use the same cookie_name and secret.
+
+ By default, this handler scans the `environ` for the REMOTE_USER
+ and REMOTE_SESSION key; if found, it is stored. It can be
+ configured to scan other `environ` keys as well -- but be careful
+ not to exceed 2-3k (so that the encoded and signed cookie does not
+ exceed 4k). You can ask it to handle other environment variables
+ by doing:
+
+ ``environ['paste.auth.cookie'].append('your.environ.variable')``
+
+
+ Constructor Arguments:
+
+ ``application``
+
+ This is the wrapped application which will have access to
+ the ``environ['REMOTE_USER']`` restored by this middleware.
+
+ ``cookie_name``
+
+ The name of the cookie used to store this content, by default
+ it is ``PASTE_AUTH_COOKIE``.
+
+ ``scanlist``
+
+ This is the initial set of ``environ`` keys to
+ save/restore to the signed cookie. By default is consists
+ only of ``REMOTE_USER`` and ``REMOTE_SESSION``; any tuple
+ or list of environment keys will work. However, be
+ careful, as the total saved size is limited to around 3k.
+
+ ``signer``
+
+ This is the signer object used to create the actual cookie
+ values, by default, it is ``AuthCookieSigner`` and is passed
+ the remaining arguments to this function: ``secret``,
+ ``timeout``, and ``maxlen``.
+
+ At this time, each cookie is individually signed. To store more
+ than the 4k of data; it is possible to sub-class this object to
+ provide different ``environ_name`` and ``cookie_name``
+ """
+ environ_name = 'paste.auth.cookie'
+ cookie_name = 'PASTE_AUTH_COOKIE'
+ signer_class = AuthCookieSigner
+ environ_class = AuthCookieEnviron
+
+ def __init__(self, application, cookie_name=None, scanlist=None,
+ signer=None, secret=None, timeout=None, maxlen=None):
+ if not signer:
+ signer = self.signer_class(secret, timeout, maxlen)
+ self.signer = signer
+ self.scanlist = scanlist or ('REMOTE_USER','REMOTE_SESSION')
+ self.application = application
+ self.cookie_name = cookie_name or self.cookie_name
+
+ def __call__(self, environ, start_response):
+ if self.environ_name in environ:
+ raise AssertionError("AuthCookie already installed!")
+ scanlist = self.environ_class(self, self.scanlist)
+ jar = get_cookies(environ)
+ if self.cookie_name in jar:
+ content = self.signer.auth(jar[self.cookie_name].value)
+ if content:
+ for pair in content.split(";"):
+ (k, v) = pair.split("=")
+ k = decode(k)
+ if k not in scanlist:
+ scanlist.append(k)
+ if k in environ:
+ continue
+ environ[k] = decode(v)
+ if 'REMOTE_USER' == k:
+ environ['AUTH_TYPE'] = 'cookie'
+ environ[self.environ_name] = scanlist
+ if "paste.httpexceptions" in environ:
+ warnings.warn("Since paste.httpexceptions is hooked in your "
+ "processing chain before paste.auth.cookie, if an "
+ "HTTPRedirection is raised, the cookies this module sets "
+ "will not be included in your response.\n")
+
+ def response_hook(status, response_headers, exc_info=None):
+ """
+ Scan the environment for keys specified in the scanlist,
+ pack up their values, signs the content and issues a cookie.
+ """
+ scanlist = environ.get(self.environ_name)
+ assert scanlist and isinstance(scanlist, self.environ_class)
+ content = []
+ for k in scanlist:
+ v = environ.get(k)
+ if v is not None:
+ if type(v) is not str:
+ raise ValueError(
+ "The value of the environmental variable %r "
+ "is not a str (only str is allowed; got %r)"
+ % (k, v))
+ content.append("%s=%s" % (encode(k), encode(v)))
+ if content:
+ content = ";".join(content)
+ content = self.signer.sign(content)
+ if six.PY3:
+ content = content.decode('utf8')
+ cookie = '%s=%s; Path=/;' % (self.cookie_name, content)
+ if 'https' == environ['wsgi.url_scheme']:
+ cookie += ' secure;'
+ response_headers.append(('Set-Cookie', cookie))
+ return start_response(status, response_headers, exc_info)
+ return self.application(environ, response_hook)
+
+middleware = AuthCookieHandler
+
+# Paste Deploy entry point:
+def make_auth_cookie(
+ app, global_conf,
+ # Should this get picked up from global_conf somehow?:
+ cookie_name='PASTE_AUTH_COOKIE',
+ scanlist=('REMOTE_USER', 'REMOTE_SESSION'),
+ # signer cannot be set
+ secret=None,
+ timeout=30,
+ maxlen=4096):
+ """
+ This middleware uses cookies to stash-away a previously
+ authenticated user (and perhaps other variables) so that
+ re-authentication is not needed. This does not implement
+ sessions; and therefore N servers can be syncronized to accept the
+ same saved authentication if they all use the same cookie_name and
+ secret.
+
+ By default, this handler scans the `environ` for the REMOTE_USER
+ and REMOTE_SESSION key; if found, it is stored. It can be
+ configured to scan other `environ` keys as well -- but be careful
+ not to exceed 2-3k (so that the encoded and signed cookie does not
+ exceed 4k). You can ask it to handle other environment variables
+ by doing:
+
+ ``environ['paste.auth.cookie'].append('your.environ.variable')``
+
+ Configuration:
+
+ ``cookie_name``
+
+ The name of the cookie used to store this content, by
+ default it is ``PASTE_AUTH_COOKIE``.
+
+ ``scanlist``
+
+ This is the initial set of ``environ`` keys to
+ save/restore to the signed cookie. By default is consists
+ only of ``REMOTE_USER`` and ``REMOTE_SESSION``; any
+ space-separated list of environment keys will work.
+ However, be careful, as the total saved size is limited to
+ around 3k.
+
+ ``secret``
+
+ The secret that will be used to sign the cookies. If you
+ don't provide one (and none is set globally) then a random
+ secret will be created. Each time the server is restarted
+ a new secret will then be created and all cookies will
+ become invalid! This can be any string value.
+
+ ``timeout``
+
+ The time to keep the cookie, expressed in minutes. This
+ is handled server-side, so a new cookie with a new timeout
+ is added to every response.
+
+ ``maxlen``
+
+ The maximum length of the cookie that is sent (default 4k,
+ which is a typical browser maximum)
+
+ """
+ if isinstance(scanlist, six.string_types):
+ scanlist = scanlist.split()
+ if secret is None and global_conf.get('secret'):
+ secret = global_conf['secret']
+ try:
+ timeout = int(timeout)
+ except ValueError:
+ raise ValueError('Bad value for timeout (must be int): %r'
+ % timeout)
+ try:
+ maxlen = int(maxlen)
+ except ValueError:
+ raise ValueError('Bad value for maxlen (must be int): %r'
+ % maxlen)
+ return AuthCookieHandler(
+ app, cookie_name=cookie_name, scanlist=scanlist,
+ secret=secret, timeout=timeout, maxlen=maxlen)
+
+__all__ = ['AuthCookieHandler', 'AuthCookieSigner', 'AuthCookieEnviron']
+
+if "__main__" == __name__:
+ import doctest
+ doctest.testmod(optionflags=doctest.ELLIPSIS)
+
diff --git a/paste/auth/digest.py b/paste/auth/digest.py
new file mode 100644
index 0000000..85e0362
--- /dev/null
+++ b/paste/auth/digest.py
@@ -0,0 +1,254 @@
+# (c) 2005 Clark C. Evans
+# This module is part of the Python Paste Project and is released under
+# the MIT License: http://www.opensource.org/licenses/mit-license.php
+# This code was written with funding by http://prometheusresearch.com
+"""
+Digest HTTP/1.1 Authentication
+
+This module implements ``Digest`` authentication as described by
+RFC 2617 [1]_ .
+
+Basically, you just put this module before your application, and it
+takes care of requesting and handling authentication requests. This
+module has been tested with several common browsers "out-in-the-wild".
+
+>>> from paste.wsgilib import dump_environ
+>>> from paste.httpserver import serve
+>>> # from paste.auth.digest import digest_password, AuthDigestHandler
+>>> realm = 'Test Realm'
+>>> def authfunc(environ, realm, username):
+... return digest_password(realm, username, username)
+>>> serve(AuthDigestHandler(dump_environ, realm, authfunc))
+serving on...
+
+This code has not been audited by a security expert, please use with
+caution (or better yet, report security holes). At this time, this
+implementation does not provide for further challenges, nor does it
+support Authentication-Info header. It also uses md5, and an option
+to use sha would be a good thing.
+
+.. [1] http://www.faqs.org/rfcs/rfc2617.html
+"""
+from paste.httpexceptions import HTTPUnauthorized
+from paste.httpheaders import *
+try:
+ from hashlib import md5
+except ImportError:
+ from md5 import md5
+import time, random
+from six.moves.urllib.parse import quote as url_quote
+import six
+
+def _split_auth_string(auth_string):
+ """ split a digest auth string into individual key=value strings """
+ prev = None
+ for item in auth_string.split(","):
+ try:
+ if prev.count('"') == 1:
+ prev = "%s,%s" % (prev, item)
+ continue
+ except AttributeError:
+ if prev == None:
+ prev = item
+ continue
+ else:
+ raise StopIteration
+ yield prev.strip()
+ prev = item
+
+ yield prev.strip()
+ raise StopIteration
+
+def _auth_to_kv_pairs(auth_string):
+ """ split a digest auth string into key, value pairs """
+ for item in _split_auth_string(auth_string):
+ (k, v) = item.split("=", 1)
+ if v.startswith('"') and len(v) > 1 and v.endswith('"'):
+ v = v[1:-1]
+ yield (k, v)
+
+def digest_password(realm, username, password):
+ """ construct the appropriate hashcode needed for HTTP digest """
+ content = "%s:%s:%s" % (username, realm, password)
+ if six.PY3:
+ content = content.encode('utf8')
+ return md5(content).hexdigest()
+
+class AuthDigestAuthenticator(object):
+ """ implementation of RFC 2617 - HTTP Digest Authentication """
+ def __init__(self, realm, authfunc):
+ self.nonce = {} # list to prevent replay attacks
+ self.authfunc = authfunc
+ self.realm = realm
+
+ def build_authentication(self, stale = ''):
+ """ builds the authentication error """
+ content = "%s:%s" % (time.time(), random.random())
+ if six.PY3:
+ content = content.encode('utf-8')
+ nonce = md5(content).hexdigest()
+
+ content = "%s:%s" % (time.time(), random.random())
+ if six.PY3:
+ content = content.encode('utf-8')
+ opaque = md5(content).hexdigest()
+
+ self.nonce[nonce] = None
+ parts = {'realm': self.realm, 'qop': 'auth',
+ 'nonce': nonce, 'opaque': opaque }
+ if stale:
+ parts['stale'] = 'true'
+ head = ", ".join(['%s="%s"' % (k, v) for (k, v) in parts.items()])
+ head = [("WWW-Authenticate", 'Digest %s' % head)]
+ return HTTPUnauthorized(headers=head)
+
+ def compute(self, ha1, username, response, method,
+ path, nonce, nc, cnonce, qop):
+ """ computes the authentication, raises error if unsuccessful """
+ if not ha1:
+ return self.build_authentication()
+ content = '%s:%s' % (method, path)
+ if six.PY3:
+ content = content.encode('utf8')
+ ha2 = md5(content).hexdigest()
+ if qop:
+ chk = "%s:%s:%s:%s:%s:%s" % (ha1, nonce, nc, cnonce, qop, ha2)
+ else:
+ chk = "%s:%s:%s" % (ha1, nonce, ha2)
+ if six.PY3:
+ chk = chk.encode('utf8')
+ if response != md5(chk).hexdigest():
+ if nonce in self.nonce:
+ del self.nonce[nonce]
+ return self.build_authentication()
+ pnc = self.nonce.get(nonce,'00000000')
+ if pnc is not None and nc <= pnc:
+ if nonce in self.nonce:
+ del self.nonce[nonce]
+ return self.build_authentication(stale = True)
+ self.nonce[nonce] = nc
+ return username
+
+ def authenticate(self, environ):
+ """ This function takes a WSGI environment and authenticates
+ the request returning authenticated user or error.
+ """
+ method = REQUEST_METHOD(environ)
+ fullpath = url_quote(SCRIPT_NAME(environ)) + url_quote(PATH_INFO(environ))
+ authorization = AUTHORIZATION(environ)
+ if not authorization:
+ return self.build_authentication()
+ (authmeth, auth) = authorization.split(" ", 1)
+ if 'digest' != authmeth.lower():
+ return self.build_authentication()
+ amap = dict(_auth_to_kv_pairs(auth))
+ try:
+ username = amap['username']
+ authpath = amap['uri']
+ nonce = amap['nonce']
+ realm = amap['realm']
+ response = amap['response']
+ assert authpath.split("?", 1)[0] in fullpath
+ assert realm == self.realm
+ qop = amap.get('qop', '')
+ cnonce = amap.get('cnonce', '')
+ nc = amap.get('nc', '00000000')
+ if qop:
+ assert 'auth' == qop
+ assert nonce and nc
+ except:
+ return self.build_authentication()
+ ha1 = self.authfunc(environ, realm, username)
+ return self.compute(ha1, username, response, method, authpath,
+ nonce, nc, cnonce, qop)
+
+ __call__ = authenticate
+
+class AuthDigestHandler(object):
+ """
+ middleware for HTTP Digest authentication (RFC 2617)
+
+ This component follows the procedure below:
+
+ 0. If the REMOTE_USER environment variable is already populated;
+ then this middleware is a no-op, and the request is passed
+ along to the application.
+
+ 1. If the HTTP_AUTHORIZATION header was not provided or specifies
+ an algorithem other than ``digest``, then a HTTPUnauthorized
+ response is generated with the challenge.
+
+ 2. If the response is malformed or or if the user's credientials
+ do not pass muster, another HTTPUnauthorized is raised.
+
+ 3. If all goes well, and the user's credintials pass; then
+ REMOTE_USER environment variable is filled in and the
+ AUTH_TYPE is listed as 'digest'.
+
+ Parameters:
+
+ ``application``
+
+ The application object is called only upon successful
+ authentication, and can assume ``environ['REMOTE_USER']``
+ is set. If the ``REMOTE_USER`` is already set, this
+ middleware is simply pass-through.
+
+ ``realm``
+
+ This is a identifier for the authority that is requesting
+ authorization. It is shown to the user and should be unique
+ within the domain it is being used.
+
+ ``authfunc``
+
+ This is a callback function which performs the actual
+ authentication; the signature of this callback is:
+
+ authfunc(environ, realm, username) -> hashcode
+
+ This module provides a 'digest_password' helper function
+ which can help construct the hashcode; it is recommended
+ that the hashcode is stored in a database, not the user's
+ actual password (since you only need the hashcode).
+ """
+ def __init__(self, application, realm, authfunc):
+ self.authenticate = AuthDigestAuthenticator(realm, authfunc)
+ self.application = application
+
+ def __call__(self, environ, start_response):
+ username = REMOTE_USER(environ)
+ if not username:
+ result = self.authenticate(environ)
+ if isinstance(result, str):
+ AUTH_TYPE.update(environ,'digest')
+ REMOTE_USER.update(environ, result)
+ else:
+ return result.wsgi_application(environ, start_response)
+ return self.application(environ, start_response)
+
+middleware = AuthDigestHandler
+
+__all__ = ['digest_password', 'AuthDigestHandler' ]
+
+def make_digest(app, global_conf, realm, authfunc, **kw):
+ """
+ Grant access via digest authentication
+
+ Config looks like this::
+
+ [filter:grant]
+ use = egg:Paste#auth_digest
+ realm=myrealm
+ authfunc=somepackage.somemodule:somefunction
+
+ """
+ from paste.util.import_string import eval_import
+ import types
+ authfunc = eval_import(authfunc)
+ assert isinstance(authfunc, types.FunctionType), "authfunc must resolve to a function"
+ return AuthDigestHandler(app, realm, authfunc)
+
+if "__main__" == __name__:
+ import doctest
+ doctest.testmod(optionflags=doctest.ELLIPSIS)
diff --git a/paste/auth/form.py b/paste/auth/form.py
new file mode 100644
index 0000000..9be82a2
--- /dev/null
+++ b/paste/auth/form.py
@@ -0,0 +1,149 @@
+# (c) 2005 Clark C. Evans
+# This module is part of the Python Paste Project and is released under
+# the MIT License: http://www.opensource.org/licenses/mit-license.php
+# This code was written with funding by http://prometheusresearch.com
+"""
+Authentication via HTML Form
+
+This is a very simple HTML form login screen that asks for the username
+and password. This middleware component requires that an authorization
+function taking the name and passsword and that it be placed in your
+application stack. This class does not include any session management
+code or way to save the user's authorization; however, it is easy enough
+to put ``paste.auth.cookie`` in your application stack.
+
+>>> from paste.wsgilib import dump_environ
+>>> from paste.httpserver import serve
+>>> from paste.auth.cookie import AuthCookieHandler
+>>> from paste.auth.form import AuthFormHandler
+>>> def authfunc(environ, username, password):
+... return username == password
+>>> serve(AuthCookieHandler(
+... AuthFormHandler(dump_environ, authfunc)))
+serving on...
+
+"""
+from paste.request import construct_url, parse_formvars
+
+TEMPLATE = """\
+<html>
+ <head><title>Please Login!</title></head>
+ <body>
+ <h1>Please Login</h1>
+ <form action="%s" method="post">
+ <dl>
+ <dt>Username:</dt>
+ <dd><input type="text" name="username"></dd>
+ <dt>Password:</dt>
+ <dd><input type="password" name="password"></dd>
+ </dl>
+ <input type="submit" name="authform" />
+ <hr />
+ </form>
+ </body>
+</html>
+"""
+
+class AuthFormHandler(object):
+ """
+ HTML-based login middleware
+
+ This causes a HTML form to be returned if ``REMOTE_USER`` is
+ not found in the ``environ``. If the form is returned, the
+ ``username`` and ``password`` combination are given to a
+ user-supplied authentication function, ``authfunc``. If this
+ is successful, then application processing continues.
+
+ Parameters:
+
+ ``application``
+
+ The application object is called only upon successful
+ authentication, and can assume ``environ['REMOTE_USER']``
+ is set. If the ``REMOTE_USER`` is already set, this
+ middleware is simply pass-through.
+
+ ``authfunc``
+
+ This is a mandatory user-defined function which takes a
+ ``environ``, ``username`` and ``password`` for its first
+ three arguments. It should return ``True`` if the user is
+ authenticated.
+
+ ``template``
+
+ This is an optional (a default is provided) HTML
+ fragment that takes exactly one ``%s`` substution
+ argument; which *must* be used for the form's ``action``
+ to ensure that this middleware component does not alter
+ the current path. The HTML form must use ``POST`` and
+ have two input names: ``username`` and ``password``.
+
+ Since the authentication form is submitted (via ``POST``)
+ neither the ``PATH_INFO`` nor the ``QUERY_STRING`` are accessed,
+ and hence the current path remains _unaltered_ through the
+ entire authentication process. If authentication succeeds, the
+ ``REQUEST_METHOD`` is converted from a ``POST`` to a ``GET``,
+ so that a redirect is unnecessary (unlike most form auth
+ implementations)
+ """
+
+ def __init__(self, application, authfunc, template=None):
+ self.application = application
+ self.authfunc = authfunc
+ self.template = template or TEMPLATE
+
+ def __call__(self, environ, start_response):
+ username = environ.get('REMOTE_USER','')
+ if username:
+ return self.application(environ, start_response)
+
+ if 'POST' == environ['REQUEST_METHOD']:
+ formvars = parse_formvars(environ, include_get_vars=False)
+ username = formvars.get('username')
+ password = formvars.get('password')
+ if username and password:
+ if self.authfunc(environ, username, password):
+ environ['AUTH_TYPE'] = 'form'
+ environ['REMOTE_USER'] = username
+ environ['REQUEST_METHOD'] = 'GET'
+ environ['CONTENT_LENGTH'] = ''
+ environ['CONTENT_TYPE'] = ''
+ del environ['paste.parsed_formvars']
+ return self.application(environ, start_response)
+
+ content = self.template % construct_url(environ)
+ start_response("200 OK", [('Content-Type', 'text/html'),
+ ('Content-Length', str(len(content)))])
+ return [content]
+
+middleware = AuthFormHandler
+
+__all__ = ['AuthFormHandler']
+
+def make_form(app, global_conf, realm, authfunc, **kw):
+ """
+ Grant access via form authentication
+
+ Config looks like this::
+
+ [filter:grant]
+ use = egg:Paste#auth_form
+ realm=myrealm
+ authfunc=somepackage.somemodule:somefunction
+
+ """
+ from paste.util.import_string import eval_import
+ import types
+ authfunc = eval_import(authfunc)
+ assert isinstance(authfunc, types.FunctionType), "authfunc must resolve to a function"
+ template = kw.get('template')
+ if template is not None:
+ template = eval_import(template)
+ assert isinstance(template, str), "template must resolve to a string"
+
+ return AuthFormHandler(app, authfunc, template)
+
+if "__main__" == __name__:
+ import doctest
+ doctest.testmod(optionflags=doctest.ELLIPSIS)
diff --git a/paste/auth/grantip.py b/paste/auth/grantip.py
new file mode 100644
index 0000000..3fe6e1c
--- /dev/null
+++ b/paste/auth/grantip.py
@@ -0,0 +1,114 @@
+# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
+# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
+"""
+Grant roles and logins based on IP address.
+"""
+import six
+from paste.util import ip4
+
+class GrantIPMiddleware(object):
+
+ """
+ On each request, ``ip_map`` is checked against ``REMOTE_ADDR``
+ and logins and roles are assigned based on that.
+
+ ``ip_map`` is a map of {ip_mask: (username, roles)}. Either
+ ``username`` or ``roles`` may be None. Roles may also be prefixed
+ with ``-``, like ``'-system'`` meaning that role should be
+ revoked. ``'__remove__'`` for a username will remove the username.
+
+ If ``clobber_username`` is true (default) then any user
+ specification will override the current value of ``REMOTE_USER``.
+ ``'__remove__'`` will always clobber the username.
+
+ ``ip_mask`` is something that `paste.util.ip4:IP4Range
+ <class-paste.util.ip4.IP4Range.html>`_ can parse. Simple IP
+ addresses, IP/mask, ip<->ip ranges, and hostnames are allowed.
+ """
+
+ def __init__(self, app, ip_map, clobber_username=True):
+ self.app = app
+ self.ip_map = []
+ for key, value in ip_map.items():
+ self.ip_map.append((ip4.IP4Range(key),
+ self._convert_user_role(value[0], value[1])))
+ self.clobber_username = clobber_username
+
+ def _convert_user_role(self, username, roles):
+ if roles and isinstance(roles, six.string_types):
+ roles = roles.split(',')
+ return (username, roles)
+
+ def __call__(self, environ, start_response):
+ addr = ip4.ip2int(environ['REMOTE_ADDR'], False)
+ remove_user = False
+ add_roles = []
+ for range, (username, roles) in self.ip_map:
+ if addr in range:
+ if roles:
+ add_roles.extend(roles)
+ if username == '__remove__':
+ remove_user = True
+ elif username:
+ if (not environ.get('REMOTE_USER')
+ or self.clobber_username):
+ environ['REMOTE_USER'] = username
+ if (remove_user and 'REMOTE_USER' in environ):
+ del environ['REMOTE_USER']
+ if roles:
+ self._set_roles(environ, add_roles)
+ return self.app(environ, start_response)
+
+ def _set_roles(self, environ, roles):
+ cur_roles = environ.get('REMOTE_USER_TOKENS', '').split(',')
+ # Get rid of empty roles:
+ cur_roles = list(filter(None, cur_roles))
+ remove_roles = []
+ for role in roles:
+ if role.startswith('-'):
+ remove_roles.append(role[1:])
+ else:
+ if role not in cur_roles:
+ cur_roles.append(role)
+ for role in remove_roles:
+ if role in cur_roles:
+ cur_roles.remove(role)
+ environ['REMOTE_USER_TOKENS'] = ','.join(cur_roles)
+
+
+def make_grantip(app, global_conf, clobber_username=False, **kw):
+ """
+ Grant roles or usernames based on IP addresses.
+
+ Config looks like this::
+
+ [filter:grant]
+ use = egg:Paste#grantip
+ clobber_username = true
+ # Give localhost system role (no username):
+ 127.0.0.1 = -:system
+ # Give everyone in 192.168.0.* editor role:
+ 192.168.0.0/24 = -:editor
+ # Give one IP the username joe:
+ 192.168.0.7 = joe
+ # And one IP is should not be logged in:
+ 192.168.0.10 = __remove__:-editor
+
+ """
+ from paste.deploy.converters import asbool
+ clobber_username = asbool(clobber_username)
+ ip_map = {}
+ for key, value in kw.items():
+ if ':' in value:
+ username, role = value.split(':', 1)
+ else:
+ username = value
+ role = ''
+ if username == '-':
+ username = ''
+ if role == '-':
+ role = ''
+ ip_map[key] = value
+ return GrantIPMiddleware(app, ip_map, clobber_username)
+
+
diff --git a/paste/auth/multi.py b/paste/auth/multi.py
new file mode 100644
index 0000000..b378fa6
--- /dev/null
+++ b/paste/auth/multi.py
@@ -0,0 +1,79 @@
+# (c) 2005 Clark C. Evans
+# This module is part of the Python Paste Project and is released under
+# the MIT License: http://www.opensource.org/licenses/mit-license.php
+# This code was written with funding by http://prometheusresearch.com
+"""
+Authentication via Multiple Methods
+
+In some environments, the choice of authentication method to be used
+depends upon the environment and is not "fixed". This middleware allows
+N authentication methods to be registered along with a goodness function
+which determines which method should be used. The following example
+demonstrates how to use both form and digest authentication in a server
+stack; by default it uses form-based authentication unless
+``*authmeth=digest`` is specified as a query argument.
+
+>>> from paste.auth import form, cookie, digest, multi
+>>> from paste.wsgilib import dump_environ
+>>> from paste.httpserver import serve
+>>>
+>>> multi = multi.MultiHandler(dump_environ)
+>>> def authfunc(environ, realm, user):
+... return digest.digest_password(realm, user, user)
+>>> multi.add_method('digest', digest.middleware, "Test Realm", authfunc)
+>>> multi.set_query_argument('digest')
+>>>
+>>> def authfunc(environ, username, password):
+... return username == password
+>>> multi.add_method('form', form.middleware, authfunc)
+>>> multi.set_default('form')
+>>> serve(cookie.middleware(multi))
+serving on...
+
+"""
+
+class MultiHandler(object):
+ """
+ Multiple Authentication Handler
+
+ This middleware provides two othogonal facilities:
+
+ - a manner to register any number of authentication middlewares
+
+ - a mechanism to register predicates which cause one of the
+ registered middlewares to be used depending upon the request
+
+ If none of the predicates returns True, then the application is
+ invoked directly without middleware
+ """
+ def __init__(self, application):
+ self.application = application
+ self.default = application
+ self.binding = {}
+ self.predicate = []
+ def add_method(self, name, factory, *args, **kwargs):
+ self.binding[name] = factory(self.application, *args, **kwargs)
+ def add_predicate(self, name, checker):
+ self.predicate.append((checker, self.binding[name]))
+ def set_default(self, name):
+ """ set default authentication method """
+ self.default = self.binding[name]
+ def set_query_argument(self, name, key = '*authmeth', value = None):
+ """ choose authentication method based on a query argument """
+ lookfor = "%s=%s" % (key, value or name)
+ self.add_predicate(name,
+ lambda environ: lookfor in environ.get('QUERY_STRING',''))
+ def __call__(self, environ, start_response):
+ for (checker, binding) in self.predicate:
+ if checker(environ):
+ return binding(environ, start_response)
+ return self.default(environ, start_response)
+
+middleware = MultiHandler
+
+__all__ = ['MultiHandler']
+
+if "__main__" == __name__:
+ import doctest
+ doctest.testmod(optionflags=doctest.ELLIPSIS)
+
diff --git a/paste/auth/open_id.py b/paste/auth/open_id.py
new file mode 100644
index 0000000..f79f7f8
--- /dev/null
+++ b/paste/auth/open_id.py
@@ -0,0 +1,413 @@
+# (c) 2005 Ben Bangert
+# This module is part of the Python Paste Project and is released under
+# the MIT License: http://www.opensource.org/licenses/mit-license.php
+"""
+OpenID Authentication (Consumer)
+
+OpenID is a distributed authentication system for single sign-on originally
+developed at/for LiveJournal.com.
+
+ http://openid.net/
+
+URL. You can have multiple identities in the same way you can have multiple
+URLs. All OpenID does is provide a way to prove that you own a URL (identity).
+And it does this without passing around your password, your email address, or
+anything you don't want it to. There's no profile exchange component at all:
+your profiile is your identity URL, but recipients of your identity can then
+learn more about you from any public, semantically interesting documents
+linked thereunder (FOAF, RSS, Atom, vCARD, etc.).
+
+``Note``: paste.auth.openid requires installation of the Python-OpenID
+libraries::
+
+ http://www.openidenabled.com/
+
+This module is based highly off the consumer.py that Python OpenID comes with.
+
+Using the OpenID Middleware
+===========================
+
+Using the OpenID middleware is fairly easy, the most minimal example using the
+basic login form thats included::
+
+ # Add to your wsgi app creation
+ from paste.auth import open_id
+
+ wsgi_app = open_id.middleware(wsgi_app, '/somewhere/to/store/openid/data')
+
+You will now have the OpenID form available at /oid on your site. Logging in will
+verify that the login worked.
+
+A more complete login should involve having the OpenID middleware load your own
+login page after verifying the OpenID URL so that you can retain the login
+information in your webapp (session, cookies, etc.)::
+
+ wsgi_app = open_id.middleware(wsgi_app, '/somewhere/to/store/openid/data',
+ login_redirect='/your/login/code')
+
+Your login code should then be configured to retrieve 'paste.auth.open_id' for
+the users OpenID URL. If this key does not exist, the user has not logged in.
+
+Once the login is retrieved, it should be saved in your webapp, and the user
+should be redirected to wherever they would normally go after a successful
+login.
+"""
+
+__all__ = ['AuthOpenIDHandler']
+
+import cgi
+import urlparse
+import re
+import six
+
+import paste.request
+from paste import httpexceptions
+
+def quoteattr(s):
+ qs = cgi.escape(s, 1)
+ return '"%s"' % (qs,)
+
+# You may need to manually add the openid package into your
+# python path if you don't have it installed with your system python.
+# If so, uncomment the line below, and change the path where you have
+# Python-OpenID.
+# sys.path.append('/path/to/openid/')
+
+from openid.store import filestore
+from openid.consumer import consumer
+from openid.oidutil import appendArgs
+
+class AuthOpenIDHandler(object):
+ """
+ This middleware implements OpenID Consumer behavior to authenticate a
+ URL against an OpenID Server.
+ """
+
+ def __init__(self, app, data_store_path, auth_prefix='/oid',
+ login_redirect=None, catch_401=False,
+ url_to_username=None):
+ """
+ Initialize the OpenID middleware
+
+ ``app``
+ Your WSGI app to call
+
+ ``data_store_path``
+ Directory to store crypto data in for use with OpenID servers.
+
+ ``auth_prefix``
+ Location for authentication process/verification
+
+ ``login_redirect``
+ Location to load after successful process of login
+
+ ``catch_401``
+ If true, then any 401 responses will turn into open ID login
+ requirements.
+
+ ``url_to_username``
+ A function called like ``url_to_username(environ, url)``, which should
+ return a string username. If not given, the URL will be the username.
+ """
+ store = filestore.FileOpenIDStore(data_store_path)
+ self.oidconsumer = consumer.OpenIDConsumer(store)
+
+ self.app = app
+ self.auth_prefix = auth_prefix
+ self.data_store_path = data_store_path
+ self.login_redirect = login_redirect
+ self.catch_401 = catch_401
+ self.url_to_username = url_to_username
+
+ def __call__(self, environ, start_response):
+ if environ['PATH_INFO'].startswith(self.auth_prefix):
+ # Let's load everything into a request dict to pass around easier
+ request = dict(environ=environ, start=start_response, body=[])
+ request['base_url'] = paste.request.construct_url(environ, with_path_info=False,
+ with_query_string=False)
+
+ path = re.sub(self.auth_prefix, '', environ['PATH_INFO'])
+ request['parsed_uri'] = urlparse.urlparse(path)
+ request['query'] = dict(paste.request.parse_querystring(environ))
+
+ path = request['parsed_uri'][2]
+ if path == '/' or not path:
+ return self.render(request)
+ elif path == '/verify':
+ return self.do_verify(request)
+ elif path == '/process':
+ return self.do_process(request)
+ else:
+ return self.not_found(request)
+ else:
+ if self.catch_401:
+ return self.catch_401_app_call(environ, start_response)
+ return self.app(environ, start_response)
+
+ def catch_401_app_call(self, environ, start_response):
+ """
+ Call the application, and redirect if the app returns a 401 response
+ """
+ was_401 = []
+ def replacement_start_response(status, headers, exc_info=None):
+ if int(status.split(None, 1)) == 401:
+ # @@: Do I need to append something to go back to where we
+ # came from?
+ was_401.append(1)
+ def dummy_writer(v):
+ pass
+ return dummy_writer
+ else:
+ return start_response(status, headers, exc_info)
+ app_iter = self.app(environ, replacement_start_response)
+ if was_401:
+ try:
+ list(app_iter)
+ finally:
+ if hasattr(app_iter, 'close'):
+ app_iter.close()
+ redir_url = paste.request.construct_url(environ, with_path_info=False,
+ with_query_string=False)
+ exc = httpexceptions.HTTPTemporaryRedirect(redir_url)
+ return exc.wsgi_application(environ, start_response)
+ else:
+ return app_iter
+
+ def do_verify(self, request):
+ """Process the form submission, initating OpenID verification.
+ """
+
+ # First, make sure that the user entered something
+ openid_url = request['query'].get('openid_url')
+ if not openid_url:
+ return self.render(request, 'Enter an identity URL to verify.',
+ css_class='error', form_contents=openid_url)
+
+ oidconsumer = self.oidconsumer
+
+ # Then, ask the library to begin the authorization.
+ # Here we find out the identity server that will verify the
+ # user's identity, and get a token that allows us to
+ # communicate securely with the identity server.
+ status, info = oidconsumer.beginAuth(openid_url)
+
+ # If the URL was unusable (either because of network
+ # conditions, a server error, or that the response returned
+ # was not an OpenID identity page), the library will return
+ # an error code. Let the user know that that URL is unusable.
+ if status in [consumer.HTTP_FAILURE, consumer.PARSE_ERROR]:
+ if status == consumer.HTTP_FAILURE:
+ fmt = 'Failed to retrieve <q>%s</q>'
+ else:
+ fmt = 'Could not find OpenID information in <q>%s</q>'
+
+ message = fmt % (cgi.escape(openid_url),)
+ return self.render(request, message, css_class='error', form_contents=openid_url)
+ elif status == consumer.SUCCESS:
+ # The URL was a valid identity URL. Now we construct a URL
+ # that will get us to process the server response. We will
+ # need the token from the beginAuth call when processing
+ # the response. A cookie or a session object could be used
+ # to accomplish this, but for simplicity here we just add
+ # it as a query parameter of the return-to URL.
+ return_to = self.build_url(request, 'process', token=info.token)
+
+ # Now ask the library for the URL to redirect the user to
+ # his OpenID server. It is required for security that the
+ # return_to URL must be under the specified trust_root. We
+ # just use the base_url for this server as a trust root.
+ redirect_url = oidconsumer.constructRedirect(
+ info, return_to, trust_root=request['base_url'])
+
+ # Send the redirect response
+ return self.redirect(request, redirect_url)
+ else:
+ assert False, 'Not reached'
+
+ def do_process(self, request):
+ """Handle the redirect from the OpenID server.
+ """
+ oidconsumer = self.oidconsumer
+
+ # retrieve the token from the environment (in this case, the URL)
+ token = request['query'].get('token', '')
+
+ # Ask the library to check the response that the server sent
+ # us. Status is a code indicating the response type. info is
+ # either None or a string containing more information about
+ # the return type.
+ status, info = oidconsumer.completeAuth(token, request['query'])
+
+ css_class = 'error'
+ openid_url = None
+ if status == consumer.FAILURE and info:
+ # In the case of failure, if info is non-None, it is the
+ # URL that we were verifying. We include it in the error
+ # message to help the user figure out what happened.
+ openid_url = info
+ fmt = "Verification of %s failed."
+ message = fmt % (cgi.escape(openid_url),)
+ elif status == consumer.SUCCESS:
+ # Success means that the transaction completed without
+ # error. If info is None, it means that the user cancelled
+ # the verification.
+ css_class = 'alert'
+ if info:
+ # This is a successful verification attempt. If this
+ # was a real application, we would do our login,
+ # comment posting, etc. here.
+ openid_url = info
+ if self.url_to_username:
+ username = self.url_to_username(request['environ'], openid_url)
+ else:
+ username = openid_url
+ if 'paste.auth_tkt.set_user' in request['environ']:
+ request['environ']['paste.auth_tkt.set_user'](username)
+ if not self.login_redirect:
+ fmt = ("If you had supplied a login redirect path, you would have "
+ "been redirected there. "
+ "You have successfully verified %s as your identity.")
+ message = fmt % (cgi.escape(openid_url),)
+ else:
+ # @@: This stuff doesn't make sense to me; why not a remote redirect?
+ request['environ']['paste.auth.open_id'] = openid_url
+ request['environ']['PATH_INFO'] = self.login_redirect
+ return self.app(request['environ'], request['start'])
+ #exc = httpexceptions.HTTPTemporaryRedirect(self.login_redirect)
+ #return exc.wsgi_application(request['environ'], request['start'])
+ else:
+ # cancelled
+ message = 'Verification cancelled'
+ else:
+ # Either we don't understand the code or there is no
+ # openid_url included with the error. Give a generic
+ # failure message. The library should supply debug
+ # information in a log.
+ message = 'Verification failed.'
+
+ return self.render(request, message, css_class, openid_url)
+
+ def build_url(self, request, action, **query):
+ """Build a URL relative to the server base_url, with the given
+ query parameters added."""
+ base = urlparse.urljoin(request['base_url'], self.auth_prefix + '/' + action)
+ return appendArgs(base, query)
+
+ def redirect(self, request, redirect_url):
+ """Send a redirect response to the given URL to the browser."""
+ response_headers = [('Content-type', 'text/plain'),
+ ('Location', redirect_url)]
+ request['start']('302 REDIRECT', response_headers)
+ return ["Redirecting to %s" % redirect_url]
+
+ def not_found(self, request):
+ """Render a page with a 404 return code and a message."""
+ fmt = 'The path <q>%s</q> was not understood by this server.'
+ msg = fmt % (request['parsed_uri'],)
+ openid_url = request['query'].get('openid_url')
+ return self.render(request, msg, 'error', openid_url, status='404 Not Found')
+
+ def render(self, request, message=None, css_class='alert', form_contents=None,
+ status='200 OK', title="Python OpenID Consumer"):
+ """Render a page."""
+ response_headers = [('Content-type', 'text/html')]
+ request['start'](str(status), response_headers)
+
+ self.page_header(request, title)
+ if message:
+ request['body'].append("<div class='%s'>" % (css_class,))
+ request['body'].append(message)
+ request['body'].append("</div>")
+ self.page_footer(request, form_contents)
+ return request['body']
+
+ def page_header(self, request, title):
+ """Render the page header"""
+ request['body'].append('''\
+<html>
+ <head><title>%s</title></head>
+ <style type="text/css">
+ * {
+ font-family: verdana,sans-serif;
+ }
+ body {
+ width: 50em;
+ margin: 1em;
+ }
+ div {
+ padding: .5em;
+ }
+ table {
+ margin: none;
+ padding: none;
+ }
+ .alert {
+ border: 1px solid #e7dc2b;
+ background: #fff888;
+ }
+ .error {
+ border: 1px solid #ff0000;
+ background: #ffaaaa;
+ }
+ #verify-form {
+ border: 1px solid #777777;
+ background: #dddddd;
+ margin-top: 1em;
+ padding-bottom: 0em;
+ }
+ </style>
+ <body>
+ <h1>%s</h1>
+ <p>
+ This example consumer uses the <a
+ href="http://openid.schtuff.com/">Python OpenID</a> library. It
+ just verifies that the URL that you enter is your identity URL.
+ </p>
+''' % (title, title))
+
+ def page_footer(self, request, form_contents):
+ """Render the page footer"""
+ if not form_contents:
+ form_contents = ''
+
+ request['body'].append('''\
+ <div id="verify-form">
+ <form method="get" action=%s>
+ Identity&nbsp;URL:
+ <input type="text" name="openid_url" value=%s />
+ <input type="submit" value="Verify" />
+ </form>
+ </div>
+ </body>
+</html>
+''' % (quoteattr(self.build_url(request, 'verify')), quoteattr(form_contents)))
+
+
+middleware = AuthOpenIDHandler
+
+def make_open_id_middleware(
+ app,
+ global_conf,
+ # Should this default to something, or inherit something from global_conf?:
+ data_store_path,
+ auth_prefix='/oid',
+ login_redirect=None,
+ catch_401=False,
+ url_to_username=None,
+ apply_auth_tkt=False,
+ auth_tkt_logout_path=None):
+ from paste.deploy.converters import asbool
+ from paste.util import import_string
+ catch_401 = asbool(catch_401)
+ if url_to_username and isinstance(url_to_username, six.string_types):
+ url_to_username = import_string.eval_import(url_to_username)
+ apply_auth_tkt = asbool(apply_auth_tkt)
+ new_app = AuthOpenIDHandler(
+ app, data_store_path=data_store_path, auth_prefix=auth_prefix,
+ login_redirect=login_redirect, catch_401=catch_401,
+ url_to_username=url_to_username or None)
+ if apply_auth_tkt:
+ from paste.auth import auth_tkt
+ new_app = auth_tkt.make_auth_tkt_middleware(
+ new_app, global_conf, logout_path=auth_tkt_logout_path)
+ return new_app