repoze.who
|
|
Overview
|
|
repoze.who is an identification and authentication framework for
|
WSGI.
|
|
Description
|
|
repoze.who's ideas are largely culled from Zope 2's Pluggable
|
Authentication Service (PAS) (but repoze.who is not dependent on
|
Zope 2 in any way). It provides no facility for authorization
|
(ensuring whether a user can or cannot perform the operation implied
|
by the request). This is considered to be the domain of the WSGI
|
application.
|
|
It attemtps to reuse implementations from paste.auth for some of its
|
functionality.
|
|
Middleware Responsibilities
|
|
repoze.who's middleware has one major function on ingress: it
|
conditionally places identification and authentication information
|
(including a REMOTE_USER value) into the WSGI environment and allows
|
the request to continue to a downstream WSGI application.
|
|
repoze.who's middleware has one major function on egress: it
|
examines the headers set by the downstream application, the WSGI
|
environment, or headers supplied by other plugins and conditionally
|
challenges for credentials.
|
|
PasteDeploy Configuration
|
|
Classifiers
|
|
repoze.who "classifies" the request on middleware ingress. Request
|
classification happens before identification and authentication. A
|
request from a browser might be classified a different way that a
|
request from an XML-RPC client. repoze.who uses request classifiers
|
to decide which other components to consult during subsequent
|
identification, authentication, and challenge steps. Plugins are
|
free to advertise themselves as willing to participate in
|
identification and authorization for a request based on this
|
classification.
|
|
The classification system is pluggable. repoze.who provides a
|
default classifier that you may use. You may extend the
|
classification system by making repoze.who aware of a different
|
classifier implementation.
|
|
Plugins
|
|
repoze.who is designed around the concept of plugins. Plugins are
|
instances that are willing to perform one or more identification-
|
and/or authentication-related duties. When you register a plugin,
|
you register a plugin factory, which is a callable that accepts
|
configuration parameters. The callable must return an instance of a
|
plugin when called. Each plugin can be configured arbitrarily using
|
values in a repoze.who-specific configuration file.
|
|
repoze.who consults the set of configured plugins when it intercepts
|
a WSGI request, and gives some subset of them a chance to influence
|
what is added to the WSGI environment.
|
|
Request (Ingress) Stages
|
|
repoze.who performs the following operations in the following order
|
during middleware ingress:
|
|
1. Request Classification
|
|
The WSGI environment is examined and the request is classified
|
into one "type" of request. The callable named as
|
'request_classifer=' within the '[general]' section is used to
|
perform the classification. It returns a value that is
|
considered to be the request classification.
|
|
2. Identification
|
|
Identifiers which nominate themselves as willing to extract data
|
for a particular class of request (as provided by the request
|
classifier) will be consulted to retrieve credentials data from
|
the environment. For example, a basic auth identifier might use
|
the HTTP_AUTHORIZATION header to find login and password
|
information. Identifiers are also responsible for providing
|
header information to set and remove authentication information
|
in the response.
|
|
3. Authentication
|
|
Authenticators which nominate themselves as willing to
|
authenticate for a particular class of request will be consulted
|
to compare information provided by the identification plugins
|
that returned credentials. For example, an htpasswd
|
authenticator might look in a file for a user record matching
|
any of the identities. If it finds one, and if the password
|
listed in the record matches the password provided by an
|
identity, the userid of the user would be returned (which would
|
be the same as the login name).
|
|
4. Metadata Provision
|
|
The identity of the authenticated user found during the
|
authentication step can be augmented with arbitrary metadata.
|
For example, a metadata provider plugin might augment the
|
identity with first, middle and last names, or a more
|
specialized metadata provider might augment the identity with a
|
list of role or group names.
|
|
Response (Egress) Stages
|
|
repoze.who performs the following operations in the following order
|
during middleware egress:
|
|
1. Challenge Decision
|
|
The WSGI environment and the status and headers returned by the
|
downstream application may be examined to determine whether a
|
challenge is required. Typically, only the status is used (if
|
it starts with "401 ", a challenge is required). This behavior
|
is pluggable.
|
|
2. Challenge
|
|
Challengers which nominate themselves as willing to execute a
|
challenge for a particular class of request (as provided by the
|
classifier) will be consulted, and one will be chosen to perform
|
a challenge. A challenger plugin can use application-returned
|
headers, the WSGI environment, and other items to determine what
|
sort of operation should be performed to actuate the challenge.
|
Note that repoze.who defers to the identifier plugin which
|
provided the identity (if any) to reset credentials at challenge
|
time; this is not the responsibility of the challenger.
|
|
Plugin Types
|
|
Identifier Plugins
|
|
You can register a plugin as willing to act as an "identifier".
|
An identifier examines the WSGI environment and attempts to
|
extract credentials from the environment. These credentials are
|
used by authenticator plugins to perform authentication. In some
|
cases, an identification plugin can "preauthenticate" an identity
|
(and can thus act as an authenticator plugin).
|
|
Authenticator Plugins
|
|
You may register a plugin as willing to act as an "authenticator".
|
Authenticator plugins are responsible for resolving a set of
|
credentials provided by an identifier plugin into a user id.
|
Typically, authenticator plugins will perform a lookup into a
|
database or some other persistent store, check the provided
|
credentials against the stored data, and return a user id if the
|
credentials can be validated.
|
|
The user id provided by an authenticator is eventually passed to
|
downstream WSGI applications in the "REMOTE_USER' environment
|
variable. Additionally, the "identity" of the user (as provided
|
by the identifier from whence the identity came) is passed along
|
to downstream application in the 'repoze.who.identity' environment
|
variable.
|
|
Metadata Provider Plugins
|
|
You may register a plugin as willing to act as a "metadata
|
provider" (aka mdprovider). Metadata provider plugins are
|
responsible for adding arbitrary information to the identity
|
dictionary for consumption by downstream applications.
|
|
Challenger Plugins
|
|
You may register a plugin as willing to act as a "challenger".
|
Challenger plugins are responsible for initiating a challenge to
|
the requesting user. Challenger plugins are invoked by repoze.who
|
when it decides a challenge is necessary. A challenge might
|
consist of displaying a form or presenting the user with a basic
|
or digest authentication dialog.
|
|
Configuration File Example
|
|
repoze.who is configured using a ConfigParser-style .INI file. The
|
configuration file has five main types of sections: plugin sections,
|
a general section, an identifiers section, an authenticators section,
|
and a challengers section. Each "plugin" section defines a
|
configuration for a particular plugin. The identifiers,
|
authenticators, and challengers sections refer to these plugins to
|
form a site configuration. The general section is general middleware
|
configuration.
|
|
Example repoze.who Configuration File (*NOTE: SCIENCE FICTION, not yet
|
implemented!*)
|
|
repoze.who is designed to be used within a PasteDeploy configuration
|
file:
|
|
[filter:who]
|
use = egg:repoze.who#who
|
config_file = %(here)s/who.ini
|
|
Below is an example of a configuration file that might be used to
|
configure the repoze.who middleware. A set of plugins are defined,
|
and they are referred to by following non-plugin sections.
|
|
In the below configuration, five plugins are defined. The form, and
|
basicauth plugins are nominated to act as challenger plugins. The
|
form, cookie, and basicauth plugins are nominated to act as
|
identification plugins. The htpasswd and sqlusers plugins are
|
nominated to act as authenticator plugins.
|
|
[plugin:form]
|
# identificaion and challenge
|
use = egg:repoze.who#form
|
login_form_qs = __do_login
|
rememberer_name = cookie
|
form = %(here)s/login_form.html
|
|
[plugin:cookie]
|
# identification
|
use = egg:repoze.who#cookie
|
cookie_name = repoze.who.auth
|
|
[plugin:basicauth]
|
# identification and challenge
|
use = egg:repoze.who#basicauth
|
realm = repoze
|
|
[plugin:htpasswd]
|
# authentication
|
use = egg:repoze.who#htpasswd
|
filename = %(here)s/users.htpasswd
|
check_fn = egg:repoze.who#crypt_check
|
|
[plugin:sqlusers]
|
# authentication
|
use = egg:repoze.who#squsersource
|
db = sqlite://database?user=foo&pass=bar
|
get_userinfo = select id, password from users
|
check_fn = egg:repoze.who#crypt_check
|
|
[plugin:properties]
|
use = egg:repoze.who#ini_metadata
|
filename = %(here)s/etc/properties.ini
|
handler = egg:repoze.who#ini_default
|
|
[plugin:roles]
|
use = egg:repoze.who#ini_metadata
|
filename = %(here)s/etc/roles.ini
|
|
[general]
|
request_classifier = egg:repoze.who#defaultrequestclassifier
|
challenge_decider = egg:repoze.who#defaultchallengedecider
|
|
[identifiers]
|
# plugin_name:classifier_name:.. or just plugin_name (good for any)
|
plugins =
|
form:browser
|
basicauth
|
|
[authenticators]
|
# plugin_name:classifier_name.. or just plugin_name (good for any)
|
plugins =
|
htpasswd
|
sqlusers
|
|
[challengers]
|
# plugin_name:classifier_name:.. or just plugin_name (good for any)
|
plugins =
|
form:browser
|
basicauth
|
|
[mdproviders]
|
plugins =
|
properties
|
roles
|
|
Further Description of Example Config
|
|
The basicauth section configures a plugin that does identification
|
and challenge for basic auth credentials. The form section
|
configures a plugin that does identification and challenge (its
|
implementation defers to the cookie plugin for identification
|
"forget" and "remember" duties, thus the "identifier_impl_name" key;
|
this is looked up at runtime). The cookie section configures a
|
plugin that does identification for cookie auth credentials. The
|
htpasswd plugin obtains its user info from a file. The sqlusers
|
plugin obtains its user info from a sqlite database.
|
|
The identifiers section provides an ordered list of plugins that are
|
willing to provide identification capability. These will be
|
consulted in the defined order. The tokens on each line of the
|
'plugins=' key are in the form
|
"plugin_name:requestclassifier_name:..." (or just "plugin_name" if
|
the plugin can be consulted regardless of the classification of the
|
request). The configuration above indicates that the system will
|
look for credentials using the form plugin (if the request is
|
classified as a browser request), then the cookie identifier
|
(unconditionally), then the basic auth plugin (unconditionally).
|
|
The authenticators section provides an ordered list of plugins that
|
provide authenticator capability. These will be consulted in the
|
defined order, so the system will look for users in the file, then
|
in the sql database when attempting to validate credentials. No
|
classification prefixes are given to restrict which of the two
|
plugins are used, so both plugins are consulted regardless of the
|
classification of the request. Each authenticator is called with
|
each set of identities found by the identifier plugins. The first
|
identity that can be authenticated is used to set "REMOTE_USER".
|
|
The mdproviders section provides an ordered list of plugins that
|
provide metadata provider capability. These will be consulted in
|
the defined order. Each will have a chance (on ingress) to provide
|
add metadata to the authenticated identity. Our example mdproviders
|
section shows two plugins configured: "properties", and "roles".
|
The (fictional) properties plugin will add information related to
|
user properties (e.g. first name, last name, phone number, etc) to
|
the identity dictionary. The (fictional) roles mdprovider will add
|
information representing the user's "roles" in the context of the
|
current request to the identity dictionary.
|
|
The challengers section provides an ordered list of plugins that
|
provide challenger capability. These will be consulted in the
|
defined order, so the system will consult the cookie auth plugin
|
first, then the basic auth plugin. Each will have a chance to
|
initiate a challenge. The above configuration indicates that the
|
form challenger will fire if it's a 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 None
|
|
import binascii
|
try:
|
auth = cookie.value.decode('base64')
|
except binascii.Error: # can't decode
|
return None
|
|
try:
|
login, password = auth.split(':', 1)
|
return {'login':login, 'password':password}
|
except ValueError: # not enough values to unpack
|
return None
|
|
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.who 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 None (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 must be a dictionary. The
|
dictionary is not required to have any particular keys or value
|
composition, although it's wise if the identification plugin looks
|
for both a login name and a password information to return at
|
least {'login':login_name, 'password':password}, as some
|
authenticator plugins may depend on presence of the names "login"
|
and "password" (e.g. the htpasswd and sql IAuthenticator plugins).
|
If an IIdentifier plugin finds no credentials, it is expected to
|
return None.
|
|
An IIdentifier plugin is also permitted to "preauthenticate" an
|
identity. If the identifier plugin knows that the identity is
|
"good" (e.g. in the case of ticket-based authentication where the
|
userid is embedded into the ticket), it can insert a special key
|
into the identity dictionary: 'repoze.who.userid'. If this key is
|
present in the identity dictionary, no authenticators will be
|
asked to authenticate the identity. This effectively allows an
|
IIdentifier plugin to become an IAuthenticator plugin when
|
breaking apart the responsibility into two separate plugins is
|
"make-work". Preauthenticated identities will be selected first
|
when deciding which identity to use for any given request. Our
|
cookie plugin doesn't use this feature.
|
|
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.
|
|
When you write a remember method, most of the work involved is
|
determining *whether or not* you need to return headers. It's
|
typical to see remember methods that compute an "old state" and a
|
"new state" and compare the two against each other in order to
|
determine if headers need to be returned. In our example
|
InsecureCookiePlugin, the "old state" is "cookie_value" and the
|
"new state" is "value".
|
|
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.who 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.who 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.
|
|
Note that our plugin does not assume that the keys 'login' or
|
'password' exist in the identity; although it requires them to do
|
"real work" it returns None if they are not present instead of
|
raising an exception. This is required by the IAuthenticator
|
interface specification.
|
|
Writing a Challenger Plugin
|
|
A challenger plugin (aka an IChallenger plugin) must do only one
|
thing on "egress": return a WSGI application which performs a
|
"challenge". A WSGI application is a callable that accepts an
|
"environ" and a "start_response" as its parameters; see "PEP 333"
|
for further definition of what a WSGI application is. 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.who as the 'IChallenger' interface. This method is
|
called when repoze.who 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.
|
|
Writing a Metadata Provider Plugin
|
|
A metadata provider plugin (aka an IMetadataProvider plugin) must do
|
only one thing (on "ingress"): "scribble" on the identity dictionary
|
provided to it when it is called. An IMetadataProvider plugin will
|
be called with the final "best" identity found during the
|
authentication phase, or not at all if no "best" identity could be
|
authenticated. Thus, each IMetadataProvider plugin will be called
|
exactly zero or one times during a request.
|
|
Here's a simple metadata provider plugin that provides "property"
|
information from a dictionary::
|
|
_DATA = { 'chris': {'first_name':'Chris', 'last_name':'McDonough'}
|
'whit': {'first_name':'Whit', 'last_name':'Morriss'}
|
}
|
|
class SimpleMetadataProvider(object):
|
def add_metadata(self, environ, identity):
|
userid = identity.get('repoze.who.userid')
|
identity.update(_DATA.get(userid))
|
|
Interfaces
|
|
See the module repoze.who.interfaces.
|