Imported from SVN by Bitbucket

This commit is contained in:
2015-03-31 20:26:20 +00:00
committed by bitbucket
commit ceb7984dec
212 changed files with 49537 additions and 0 deletions

View 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.
"""

View 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)

View 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)

View 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)))

View 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)

View 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)

View 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)

View 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)

View 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)

View 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&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, 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