Imported from SVN by Bitbucket
This commit is contained in:
9
Paste-1.7.5.1-py2.6.egg/paste/auth/__init__.py
Executable file
9
Paste-1.7.5.1-py2.6.egg/paste/auth/__init__.py
Executable file
@@ -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.
|
||||
"""
|
||||
396
Paste-1.7.5.1-py2.6.egg/paste/auth/auth_tkt.py
Executable file
396
Paste-1.7.5.1-py2.6.egg/paste/auth/auth_tkt.py
Executable file
@@ -0,0 +1,396 @@
|
||||
# (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:
|
||||
from hashlib import md5
|
||||
except ImportError:
|
||||
from md5 import md5
|
||||
import Cookie
|
||||
from paste import request
|
||||
from urllib import quote as url_quote
|
||||
from urllib import unquote as url_unquote
|
||||
|
||||
|
||||
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 cookie name and
|
||||
timestamp.
|
||||
|
||||
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):
|
||||
self.secret = secret
|
||||
self.userid = userid
|
||||
self.ip = ip
|
||||
self.tokens = ','.join(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
|
||||
|
||||
def digest(self):
|
||||
return calculate_digest(
|
||||
self.ip, self.time, self.secret, self.userid, self.tokens,
|
||||
self.user_data)
|
||||
|
||||
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 = Cookie.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):
|
||||
"""
|
||||
Parse the ticket, returning (timestamp, userid, tokens, user_data).
|
||||
|
||||
If the ticket cannot be parsed, ``BadTicket`` will be raised with
|
||||
an explanation.
|
||||
"""
|
||||
ticket = ticket.strip('"')
|
||||
digest = ticket[:32]
|
||||
try:
|
||||
timestamp = int(ticket[32:40], 16)
|
||||
except ValueError, e:
|
||||
raise BadTicket('Timestamp is not a hex integer: %s' % e)
|
||||
try:
|
||||
userid, data = ticket[40:].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)
|
||||
|
||||
if expected != digest:
|
||||
raise BadTicket('Digest signature is not correct',
|
||||
expected=(expected, digest))
|
||||
|
||||
tokens = tokens.split(',')
|
||||
|
||||
return (timestamp, userid, tokens, user_data)
|
||||
|
||||
|
||||
def calculate_digest(ip, timestamp, secret, userid, tokens, user_data):
|
||||
secret = maybe_encode(secret)
|
||||
userid = maybe_encode(userid)
|
||||
tokens = maybe_encode(tokens)
|
||||
user_data = maybe_encode(user_data)
|
||||
digest0 = md5(
|
||||
encode_ip_timestamp(ip, timestamp) + secret + userid + '\0'
|
||||
+ tokens + '\0' + user_data).hexdigest()
|
||||
digest = md5(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.
|
||||
|
||||
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):
|
||||
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
|
||||
|
||||
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)
|
||||
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)
|
||||
122
Paste-1.7.5.1-py2.6.egg/paste/auth/basic.py
Executable file
122
Paste-1.7.5.1-py2.6.egg/paste/auth/basic.py
Executable file
@@ -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)
|
||||
99
Paste-1.7.5.1-py2.6.egg/paste/auth/cas.py
Executable file
99
Paste-1.7.5.1-py2.6.egg/paste/auth/cas.py
Executable file
@@ -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.
|
||||
"""
|
||||
import urllib
|
||||
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 = urllib.urlencode(
|
||||
{'service': service,'ticket': ticket})
|
||||
requrl = authority + "validate?" + args
|
||||
result = urllib.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 = urllib.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)))
|
||||
396
Paste-1.7.5.1-py2.6.egg/paste/auth/cookie.py
Executable file
396
Paste-1.7.5.1-py2.6.egg/paste/auth/cookie.py
Executable file
@@ -0,0 +1,396 @@
|
||||
# (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, 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('x', '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 reduce((lambda a, (b, c): a.replace(b, c)), 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 """
|
||||
return ''.join(random.sample(_all_chars, 64))
|
||||
|
||||
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, basestring):
|
||||
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.
|
||||
"""
|
||||
cookie = base64.encodestring(
|
||||
hmac.new(self.secret, content, sha1).digest() +
|
||||
make_time(time.time() + 60*self.timeout) +
|
||||
content)
|
||||
cookie = cookie.replace("/", "_").replace("=", "~")
|
||||
cookie = cookie.replace('\n', '').replace('\r', '')
|
||||
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 jar.has_key(self.cookie_name):
|
||||
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)
|
||||
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, basestring):
|
||||
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)
|
||||
|
||||
214
Paste-1.7.5.1-py2.6.egg/paste/auth/digest.py
Executable file
214
Paste-1.7.5.1-py2.6.egg/paste/auth/digest.py
Executable file
@@ -0,0 +1,214 @@
|
||||
# (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 urllib import quote as url_quote
|
||||
|
||||
def digest_password(realm, username, password):
|
||||
""" construct the appropriate hashcode needed for HTTP digest """
|
||||
return md5("%s:%s:%s" % (username, realm, password)).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 """
|
||||
nonce = md5(
|
||||
"%s:%s" % (time.time(), random.random())).hexdigest()
|
||||
opaque = md5(
|
||||
"%s:%s" % (time.time(), random.random())).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()
|
||||
ha2 = md5('%s:%s' % (method, path)).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 response != md5(chk).hexdigest():
|
||||
if nonce in self.nonce:
|
||||
del self.nonce[nonce]
|
||||
return self.build_authentication()
|
||||
pnc = self.nonce.get(nonce,'00000000')
|
||||
if 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 = {}
|
||||
for itm in auth.split(", "):
|
||||
(k,v) = [s.strip() for s in itm.split("=", 1)]
|
||||
amap[k] = v.replace('"', '')
|
||||
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)
|
||||
149
Paste-1.7.5.1-py2.6.egg/paste/auth/form.py
Executable file
149
Paste-1.7.5.1-py2.6.egg/paste/auth/form.py
Executable file
@@ -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)
|
||||
113
Paste-1.7.5.1-py2.6.egg/paste/auth/grantip.py
Executable file
113
Paste-1.7.5.1-py2.6.egg/paste/auth/grantip.py
Executable file
@@ -0,0 +1,113 @@
|
||||
# (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.
|
||||
"""
|
||||
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, basestring):
|
||||
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 = 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)
|
||||
|
||||
|
||||
79
Paste-1.7.5.1-py2.6.egg/paste/auth/multi.py
Executable file
79
Paste-1.7.5.1-py2.6.egg/paste/auth/multi.py
Executable file
@@ -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)
|
||||
|
||||
412
Paste-1.7.5.1-py2.6.egg/paste/auth/open_id.py
Executable file
412
Paste-1.7.5.1-py2.6.egg/paste/auth/open_id.py
Executable file
@@ -0,0 +1,412 @@
|
||||
# (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 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 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, basestring):
|
||||
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
|
||||
Reference in New Issue
Block a user