[Repoze-checkins] r767 - in repoze.pam/trunk: . repoze/pam/etc
Chris McDonough
chrism at agendaless.com
Tue Mar 4 09:49:23 UTC 2008
Author: Chris McDonough <chrism at agendaless.com>
Date: Tue Mar 4 04:49:23 2008
New Revision: 767
Log:
Fix missing use and typo in sample-config.ini
Add docs about writing plugins.
Modified:
repoze.pam/trunk/README.txt
repoze.pam/trunk/repoze/pam/etc/sample-config.ini
Modified: repoze.pam/trunk/README.txt
==============================================================================
--- repoze.pam/trunk/README.txt (original)
+++ repoze.pam/trunk/README.txt Tue Mar 4 04:49:23 2008
@@ -283,7 +283,241 @@
browser request, and the basic auth challenger will fire if it's not
(fallback).
+Writing An Identifier Plugin
+
+ An identifier plugin (aka an IIdentifier plugin) must do three
+ things: extract credentials from the request and turn them into an
+ "identity", "remember" credentials, and "forget" credentials.
+
+ Here's a simple cookie identification plugin that does these three
+ things::
+
+ class InsecureCookiePlugin(object):
+
+ def __init__(self, cookie_name):
+ self.cookie_name = cookie_name
+
+ def identify(self, environ):
+ cookies = get_cookies(environ)
+ cookie = cookies.get(self.cookie_name)
+
+ if cookie is None:
+ return {}
+
+ import binascii
+ try:
+ auth = cookie.value.decode('base64')
+ except binascii.Error: # can't decode
+ return {}
+
+ try:
+ login, password = auth.split(':', 1)
+ return {'login':login, 'password':password}
+ except ValueError: # not enough values to unpack
+ return {}
+
+ def remember(self, environ, identity):
+ cookie_value = '%(login)s:%(password)s' % identity
+ cookie_value = cookie_value.encode('base64').rstrip()
+ from paste.request import get_cookies
+ cookies = get_cookies(environ)
+ existing = cookies.get(self.cookie_name)
+ value = getattr(existing, 'value', None)
+ if value != cookie_value:
+ # return a Set-Cookie header
+ set_cookie = '%s=%s; Path=/;' % (self.cookie_name, cookie_value)
+ return [('Set-Cookie', set_cookie)]
+
+ def forget(self, environ, identity):
+ # return a expires Set-Cookie header
+ expired = ('%s=""; Path=/; Expires=Sun, 10-May-1971 11:59:00 GMT' %
+ self.cookie_name)
+ return [('Set-Cookie', expired)]
+
+ def __repr__(self):
+ return '<%s %s>' % (self.__class__.__name__, id(self))
+
+ Note that the plugin implements three "interface" methods:
+ "identify", "forget" and "remember". The formal specification for
+ the arguments and return values expected from these methods are
+ available in the "interfaces.py" file in repoze.pam as the
+ 'IIdentifier' interface, but let's examine them less formally one at
+ a time.
+
+ identify(environ) --
+
+ The 'identify' method of our InsecureCookiePlugin accepts a single
+ argument "environ". This will be the WSGI environment dictionary.
+ Our plugin attempts to grub through the cookies sent by the
+ client, trying to find one that matches our cookie name. If it
+ finds one that matches, it attempts to decode it and turn it into
+ a login and a password, which it returns as values in a
+ dictionary. This dictionary is thereafter known as an "identity".
+ If it finds no credentials in cookies, it returns an empty
+ dictionary (which is not considered an identity).
+
+ More generally, the 'identify' method of an IIdentifier plugin is
+ called once on WSGI request "ingress", and it is expected to grub
+ arbitrarily through the WSGI environment looking for credential
+ information. In our above plugin, the credential information is
+ expected to be in a cookie but credential information could be in
+ a cookie, a form field, basic/digest auth information, a header, a
+ WSGI environment variable set by some upstream middleware or
+ whatever else someone might use to stash authentication
+ information. If the plugin finds credentials in the request, it's
+ expected to return an "identity": this is a dictionary of (at
+ least) the form {'login':login_name, 'password':password}. It may
+ place other information in the dictionary for use by special
+ IAuthenticator plugins, but these are the two required fields in
+ the dictionary. If it finds no credentials, it is expected to
+ return an empty dictionary.
+
+ remember(environ, identity) --
+
+ If we've passed a REMOTE_USER to the WSGI application during
+ ingress (as a result of providing an identity that could be
+ authenticated), and the downstream application doesn't kick back
+ with an unauthorized response, on egress we want the requesting
+ client to "remember" the identity we provided if there's some way
+ to do that and if he hasn't already, in order to ensure he will
+ pass it back to us on subsequent requests without requiring
+ another login. The remember method of an IIdentifier plugin is
+ called for each non-unauthenticated response. It is the
+ responsibility of the IIdentifier plugin to conditionally return
+ HTTP headers that will cause the client to remember the
+ credentials implied by "identity".
+
+ Our InsecureCookiePlugin implements the "remember" method by
+ returning headers which set a cookie if and only if one is not
+ already set with the same name and value in the WSGI environment.
+ These headers will be tacked on to the response headers provided
+ by the downstream application during the response.
+
+ forget(environ, identity) --
+
+ Eventually the WSGI application we're serving will issue a "401
+ Unauthorized" or another status signifying that the request could
+ not be authorized. repoze.pam intercepts this status and calls
+ IIdentifier plugins asking them to "forget" the credentials
+ implied by the identity. It is the "forget" method's job at this
+ point to return HTTP headers that will effectively clear any
+ credentials on the requesting client implied by the "identity"
+ argument.
+
+ Our InsecureCookiePlugin implements the "forget" method by
+ returning a header which resets the cookie that was set earlier by
+ the remember method to one that expires in the past (on my
+ birthday, in fact). This header will be tacked onto the response
+ headers provided by the downstream application.
+
+Writing an Authenticator Plugin
+
+ An authenticator plugin (aka an IAuthenticator plugin) must do only
+ one thing (on "ingress"): accept an identity and check if the
+ identity is "good". If the identity is good, it should return a
+ "user id". This user id may or may not be the same as the "login"
+ provided by the user. An IAuthenticator plugin will be called for
+ each identity found during the identification phase (there may be
+ multiple identities for a single request, as there may be multiple
+ IIdentifier plugins active at any given time), so it may be called
+ multiple times in the same request.
+
+ Here's a simple authenticator plugin that attempts to match an
+ identity against ones defined in an "htpasswd" file that does just
+ that::
+
+ class SimpleHTPasswdPlugin(object):
+
+ def __init__(self, filename):
+ self.filename = filename
+
+ # IAuthenticatorPlugin
+ def authenticate(self, environ, identity):
+ try:
+ login = identity['login']
+ password = identity['password']
+ except KeyError:
+ return None
+
+ f = open(self.filename, 'r')
+
+ for line in f:
+ try:
+ username, hashed = line.rstrip().split(':', 1)
+ except ValueError:
+ continue
+ if username == login:
+ if crypt_check(password, hashed):
+ return username
+ return None
+
+ def crypt_check(password, hashed):
+ from crypt import crypt
+ salt = hashed[:2]
+ return hashed == crypt(password, salt)
+
+ Note that the plugin implements a single "interface" method:
+ "authenticate". The formal specification for the arguments and
+ return values expected from this method is available in the
+ "interfaces.py" file in repoze.pam as the 'IAuthenticator'
+ interface, but we can explore this a little further here.
+
+ The 'authenticate' method accepts two arguments: the WSGI
+ environment and an identity. Our SimpleHTPasswdPlugin
+ 'authenticate' implementation grabs the login and password out of
+ the identity and attempts to find the login in the htpasswd file.
+ If it finds it, it compares the crypted version of the password
+ provided by the user to the crypted version stored in the htpasswd
+ file, and finally, if they match, it returns the login. If they do
+ not match, it returns None.
+
+Writing a Challenger Plugin
+
+ A challenger plugin (aka an IChallenger plugin) must do only one
+ thing (on "egress"): return a WSGI application (see PEP 333 for the
+ definition of a WSGI application) which performs a "challenge." A
+ challenge asks the user for credentials.
+
+ Here's an example of a simple challenger plugin::
+
+ from paste.httpheaders import WWW_AUTHENTICATE
+ from paste.httpexceptions import HTTPUnauthorized
+
+ class BasicAuthChallengerPlugin(object):
+
+ def __init__(self, realm):
+ self.realm = realm
+
+ # IChallenger
+ def challenge(self, environ, status, app_headers, forget_headers):
+ head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
+ if head[0] not in forget_headers:
+ head = head + forget_headers
+ return HTTPUnauthorized(headers=head)
+
+ Note that the plugin implements a single "interface" method:
+ "challenge". The formal specification for the arguments and return
+ values expected from this method is available in the "interfaces.py"
+ file in repoze.pam as the 'IChallenger' interface. This method is
+ called when repoze.pam determines that the application has returned
+ an "unauthorized" response (e.g. a 401). Only one challenger will
+ be consulted during "egress" as necessary (the first one to return a
+ non-None response). The challenge method takes environ (the WSGI
+ environment), 'status' (the status as set by the downstream
+ application), the "app_headers" (headers returned by the
+ application), and the "forget_headers" (headers returned by all
+ participating IIdentifier plugins whom were asked to "forget" this
+ user).
+
+ Our BasicAuthChallengerPlugin takes advantage of the fact that the
+ HTTPUnauthorized exception imported from paste.httpexceptions can be
+ used as a WSGI application. It first makes sure that we don't
+ repeat headers if an identification plugin has already set a
+ "WWW-Authenticate" header like ours, then it returns an instance of
+ HTTPUnauthorized, passing in merged headers. This will cause a
+ basic authentication dialog to be presented to the user.
+
Interfaces
- See repoze.pam.interfaces.
+ See the module repoze.pam.interfaces.
Modified: repoze.pam/trunk/repoze/pam/etc/sample-config.ini
==============================================================================
--- repoze.pam/trunk/repoze/pam/etc/sample-config.ini (original)
+++ repoze.pam/trunk/repoze/pam/etc/sample-config.ini Tue Mar 4 04:49:23 2008
@@ -1,5 +1,6 @@
[plugin:form]
-# identificaion and challenge
+# identification and challenge
+use = egg:repoze.pam#form
login_form_qs = __do_login
identifier_impl_name = cookie
More information about the Repoze-checkins
mailing list