repoze.who as 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 as 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.
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 than 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 request 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 request classifier implementation.
repoze.who uses a “challenge decider” to decide whether the response returned from a downstream application requires a challenge plugin to fire. When using the default challenge decider, only the status is used (if it starts with 401, a challenge is required).
repoze.who also provides an alternate challenge decider, repoze.who.classifiers.passthrough_challenge_decider, which avoids challenging 401 responses which have been “pre-challenged” by the application.
You may supply a different challenge decider as necessary.
repoze.who has core functionality designed around the concept of plugins. Plugins are instances that are willing to perform one or more identification- and/or authentication-related duties. Each plugin can be configured arbitrarily.
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 repoze.who does for the current request.
Note
As of repoze.who 1.0.7, the repoze.who.plugins package is a namespace package, intended to make it possible for people to ship eggs which are who plugins as, e.g. repoze.who.plugins.mycoolplugin.
repoze.who performs duties both on middleware “ingress” and on middleware “egress”.
repoze.who performs the following operations in the following order during middleware ingress:
Request Classification
The WSGI environment is examined and the request is classified into one “type” of request. The callable named as the classifer argument to the repoze.who middleware constructor is used to perform the classification. It returns a value that is considered to be the request classification (a single string).
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 during egress.
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).
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.
repoze.who performs the following operations in the following order during middleware egress:
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, and the challenge decider returns True). This behavior is pluggable. It is replaced by changing the challenge_decider argument to the middleware. If a challenge is required, the challenge decider will return True; if it’s not, it will return False.
Challenge
If the challenge decider returns True, 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. This is known as “forgetting” credentials.
Remember
The identifier plugin that the “best” set of credentials came from (if any) will be consulted to “remember” these credentials if the challenge decider returns False.
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).
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.
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. For instance, a metadata provider plugin may add “group” information to the the identity.
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.
repoze.who ships with a variety of default plugins that do authentication, identification, challenge and metadata provision.
An AuthTktCookiePlugin is an IIdentifier plugin which remembers its identity state in a client-side cookie. This plugin uses the paste.auth.auth_tkt“auth ticket” protocol. It should be instantiated passing a secret, which is used to encrypt the cookie on the client side and decrypt the cookie on the server side. The cookie name used to store the cookie value can be specified using the cookie_name parameter. If secure is False, the cookie will be sent across any HTTP or HTTPS connection; if it is True, the cookie will be sent only across an HTTPS connection. If include_ip is True, the REMOTE_ADDR of the WSGI environment will be placed in the cookie. If timeout is specfied, it is the maximum age in seconds allowed for a cookie. If reissue_time is specified, when we encounter a cookie that is older than the reissue time (in seconds), but younger that the timeout, a new cookie will be issued. If timeout is specified, you must also set reissue_time to a lower value.
If userid_checker is provided, it must be a dotted Python name that resolves to a function which accepts a userid and returns a boolean True or False, indicating whether that user exists in a database. This is a workaround. Due to a design bug in repoze.who, the only way who can check for user existence is to use one or more IAuthenticator plugin authenticate methods. If an IAuthenticator’s authenticate method returns true, it means that the user exists. However most IAuthenticator plugins expect both a username and a password, and will return False unconditionally if both aren’t supplied. This means that an authenticator can’t be used to check if the user “only” exists. The identity provided by an auth_tkt does not contain a password to check against. The actual design bug in repoze.who is this: when a user presents credentials from an auth_tkt, he is considered “preauthenticated”. IAuthenticator.authenticate is just never called for a “preauthenticated” identity, which works fine, but it means that the user will be considered authenticated even if you deleted the user’s record from whatever database you happen to be using. However, if you use a userid_checker, you can ensure that a user exists for the auth_tkt supplied userid. If the userid_checker returns False, the auth_tkt credentials are considered “no good”.
Note
Using the include_ip setting for public-facing applications may cause problems for some users. One study reports that as many as 3% of users change their IP addresses legitimately during a session.
A BasicAuthPlugin plugin is both an IIdentifier and IChallenger plugin that implements the Basic Access Authentication scheme described in RFC 2617. It looks for credentials within the HTTP-Authorization header sent by browsers. It challenges by sending an WWW-Authenticate header to the browser. The single argument realm indicates the basic auth realm that should be sent in the WWW-Authenticate header.
A InsecureCookiePlugin is an IIdentifier plugin. It stores identification information in an insecure form (the base64 value of the username and password separated by a colon) in a client-side cookie. It accepts a single argument named cookie_name. This is the cookie name of the cookie used to store the identification information.
A FormPlugin is both an IIdentifier and IChallenger plugin. It intercepts form POSTs to gather identification at ingress and conditionally displays a login form at egress if challenge is required. login_form_qs is a query string name used to denote that a form POST is destined for the form plugin (anything unique is fine), rememberer_name is the “configuration name” of another IIdentifier plugin that will be used to perform remember and forget duties for the FormPlugin (it does not do these itself). For example, if you have a cookie identification plugin named cookie defined in your middleware configuration, you might set rememberer_name to cookie. formbody is a literal string that should be displayed as the form body. formcallable is a callable that will return a form body if formbody is None. If both formbody and formcallable are None, a default form is used.
A RedirectingFormPlugin is both an IIdentifier and IChallenger plugin. It intercepts form POSTs to gather identification at ingress and conditionally redirects a login form at egress if challenge is required (as opposed to the FormPlugin, it does not handle its own form generation). login_form_url is a URL that should be redirected to when a challenge is required. login_handler_path is the path that the form will POST to, signifying that the plugin should gather credentials. logout_handler_path is a path that can be called to log the current user out when visited. rememberer_name is the configuration name of another IIdentifier plugin that will be used to perform remember and forget duties for the RedirectingFormPlugin (it does not do these itself). For example, if you have a cookie identification plugin named cookie defined in your middleware configuration, you might set rememberer_name to cookie.
A HTPasswdPlugin is an IAuthenticator implementation which compares identity information against an Apache-style htpasswd file. The filename argument should be an absolute path to the htpasswd file’ the check argument is a callable which takes two arguments: “password” and “hashed”, where the “password” argument is the unencrypted password provided by the identifier plugin, and the hashed value is the value stored in the htpasswd file. If the hashed value of the password matches the hash, this callable should return True. A default implementation named crypt_check is available for use as a check function (on UNIX) as repoze.who.plugins.htpasswd:crypt_check; it assumes the values in the htpasswd file are encrypted with the UNIX crypt function.
A SQLAuthenticatorPlugin is an IAuthenticator implementation which compares login-password identity information against data in an arbitrary SQL database. The query argument should be a SQL query that returns two columns in a single row considered to be the user id and the password respectively. The SQL query should contain Python-DBAPI style substitution values for %(login), e.g. SELECT user_id, password FROM users WHERE login = %(login). The conn_factory argument should be a callable that returns a DBAPI database connection. The compare_fn argument should be a callable that accepts two arguments: cleartext and stored_password_hash. It should compare the hashed version of cleartext and return True if it matches the stored password hash, otherwise it should return False. A comparison function named default_password_compare exists in the repoze.who.plugins.sql module demonstrating this. The SQLAuthenticatorPlugin‘s authenticate method will return the user id of the user unchanged to repoze.who.
A SQLMetatadaProviderPlugin is an IMetadataProvider implementation which adds arbitrary metadata to the identity on ingress using data from an arbitrary SQL database. The name argument should be a string. It will be used as a key in the identity dictionary. The query argument should be a SQL query that returns arbitrary data from the database in a form that accepts Python-binding style DBAPI arguments. It should expect that a __userid value will exist in the dictionary that is bound. The SQL query should contain Python-DBAPI style substitution values for (at least) %(__userid), e.g. SELECT group FROM groups WHERE user_id = %(__userid). The conn_factory argument should be a callable that returns a DBAPI database connection. The filter argument should be a callable that accepts the result of the DBAPI fetchall based on the SQL query. It should massage the data into something that will be set in the environment under the name key.
The primary method of configuring the repoze.who middleware is to use straight Python code, meant to be consumed by frameworks which construct and compose middleware pipelines without using a configuration file.
In the middleware constructor: app is the “next” application in the WSGI pipeline. identifiers is a sequence of IIdentifier plugins, challengers is a sequence of IChallenger plugins, mdproviders is a sequence of IMetadataProvider plugins. Any of these can be specified as the empty sequence. classifier is a request classifier callable, challenge_decider is a challenge decision callable. log_stream is a stream object (an object with a write method) or a logging.Logger object, log_level is a numeric value that maps to the logging module’s notion of log levels, remote_user_key is the key in which the REMOTE_USER (userid) value should be placed in the WSGI environment for consumption by downstream applications.
An example configuration which uses the default plugins follows:
from repoze.who.middleware import PluggableAuthenticationMiddleware
from repoze.who.interfaces import IIdentifier
from repoze.who.interfaces import IChallenger
from repoze.who.plugins.basicauth import BasicAuthPlugin
from repoze.who.plugins.auth_tkt import AuthTktCookiePlugin
from repoze.who.plugins.cookie import InsecureCookiePlugin
from repoze.who.plugins.form import FormPlugin
from repoze.who.plugins.htpasswd import HTPasswdPlugin
io = StringIO()
salt = 'aa'
for name, password in [ ('admin', 'admin'), ('chris', 'chris') ]:
io.write('%s:%s\n' % (name, password))
io.seek(0)
def cleartext_check(password, hashed):
return password == hashed
htpasswd = HTPasswdPlugin(io, cleartext_check)
basicauth = BasicAuthPlugin('repoze.who')
auth_tkt = AuthTktCookiePlugin('secret', 'auth_tkt')
form = FormPlugin('__do_login', rememberer_name='auth_tkt')
form.classifications = { IIdentifier:['browser'],
IChallenger:['browser'] } # only for browser
identifiers = [('form', form),('auth_tkt',auth_tkt),('basicauth',basicauth)]
authenticators = [('htpasswd', htpasswd)]
challengers = [('form',form), ('basicauth',basicauth)]
mdproviders = []
from repoze.who.classifiers import default_request_classifier
from repoze.who.classifiers import default_challenge_decider
log_stream = None
import os
if os.environ.get('WHO_LOG'):
log_stream = sys.stdout
middleware = PluggableAuthenticationMiddleware(
app,
identifiers,
authenticators,
challengers,
mdproviders,
default_request_classifier,
default_challenge_decider,
log_stream = log_stream,
log_level = logging.DEBUG
)
The above example configures the repoze.who middleware with:
The rest of the middleware configuration is for values like logging and the classifier and decider implementations. These use the “stock” implementations.
Note
The app referred to in the example is the “downstream” WSGI application that who is wrapping.
repoze.who may be 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.
To configure repoze.who in Python, using an .INI file, call the make_middleware_with_config entry point, passing the right-hand application and the path to the confi file
from repoze.who.config import make_middleware_with_config
who = make_middleware_with_config(app, '/path/to/who.ini')
repoze.who‘s configuration file can be pointed to within a PasteDeploy configuration file
[filter:who]
use = egg:repoze.who#config
config_file = %(here)s/who.ini
log_file = stdout
log_level = debug
Below is an example of a configuration file (what config_file might point at above ) 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 = repoze.who.plugins.form:make_plugin
login_form_qs = __do_login
rememberer_name = auth_tkt
form = %(here)s/login_form.html
[plugin:auth_tkt]
# identification
use = repoze.who.plugins.auth_tkt:make_plugin
secret = s33kr1t
cookie_name = oatmeal
secure = False
include_ip = False
[plugin:basicauth]
# identification and challenge
use = repoze.who.plugins.basicauth:make_plugin
realm = 'sample'
[plugin:htpasswd]
# authentication
use = repoze.who.plugins.htpasswd:make_plugin
filename = %(here)s/passwd
check_fn = repoze.who.plugins.htpasswd:crypt_check
[plugin:sqlusers]
# authentication
use = repoze.who.plugins.sql:make_authenticator_plugin
query = "SELECT userid, password FROM users where login = %(login)s;"
conn_factory = repoze.who.plugins.sql:make_psycopg_conn_factory
compare_fn = repoze.who.plugins.sql:default_password_compare
[plugin:sqlproperties]
name = properties
use = repoze.who.plugins.sql:make_metadata_plugin
query = "SELECT firstname, lastname FROM users where userid = %(__userid)s;"
filter = my.package:filter_propmd
conn_factory = repoze.who.plugins.sql:make_psycopg_conn_factory
[general]
request_classifier = repoze.who.classifiers:default_request_classifier
challenge_decider = repoze.who.classifiers:default_challenge_decider
remote_user_key = REMOTE_USER
[identifiers]
# plugin_name;classifier_name:.. or just plugin_name (good for any)
plugins =
form;browser
auth_tkt
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 =
sqlproperties
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 auth_tkt 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 Postgres 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 one plugin configured: “sqlproperties”. The sqlproperties plugin will add information related to user properties (e.g. first name and last name) 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).
repoze.who can be extended arbitrarily through the creation of plugins. Plugins are of one of four types: identifier plugins, authenticator plugins, metadata provider plugins, and challenge plugins.
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))
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.
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.
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.
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)
An IAuthenticator plugin implements one “interface” method: “authentictate”. 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 IAuthenticator interface, but let’s examine this method here less formally.
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
Our plugin’s authenticate method 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.
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.
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')
info = _DATA.get(userid)
if info is not None:
identity.update(info)
Arbitrarily add information to the identity dict based in other data in the environment or identity. Our plugin adds first_name and last_name values to the identity if the userid matches chris or whit.
class IPlugin(Interface):
pass
class IRequestClassifier(IPlugin):
""" On ingress: classify a request.
"""
def __call__(environ):
""" environ -> request classifier string
This interface is responsible for returning a string
value representing a request classification.
o 'environ' is the WSGI environment.
"""
class IChallengeDecider(IPlugin):
""" On egress: decide whether a challenge needs to be presented
to the user.
"""
def __call__(environ, status, headers):
""" args -> True | False
o 'environ' is the WSGI environment.
o 'status' is the HTTP status as returned by the downstream
WSGI application.
o 'headers' are the headers returned by the downstream WSGI
application.
This interface is responsible for returning True if
a challenge needs to be presented to the user, False otherwise.
"""
class IIdentifier(IPlugin):
"""
On ingress: Extract credentials from the WSGI environment and
turn them into an identity.
On egress (remember): Conditionally set information in the response headers
allowing the remote system to remember this identity.
On egress (forget): Conditionally set information in the response
headers allowing the remote system to forget this identity (during
a challenge).
"""
def identify(environ):
""" On ingress:
environ -> { k1 : v1
, ...
, kN : vN
} | None
o 'environ' is the WSGI environment.
o If credentials are found, the returned identity mapping will
contain an arbitrary set of key/value pairs. If the
identity is based on a login and password, the environment
is recommended to contain at least 'login' and 'password'
keys as this provides compatibility between the plugin and
existing authenticator plugins. If the identity can be
'preauthenticated' (e.g. if the userid is embedded in the
identity, such as when we're using ticket-based
authentication), the plugin should set the userid in the
special 'repoze.who.userid' key; no authenticators will be
asked to authenticate the identity thereafer.
o Return None to indicate that the plugin found no appropriate
credentials.
o Only IIdentifier plugins which match one of the the current
request's classifications will be asked to perform
identification.
o An identifier plugin is permitted to add a key to the
environment named 'repoze.who.application', which should be
an arbitrary WSGI application. If an identifier plugin does
so, this application is used instead of the downstream
application set up within the middleware. This feature is
useful for identifier plugins which need to perform
redirection to obtain credentials. If two identifier
plugins add a 'repoze.who.application' WSGI application to
the environment, the last one consulted will"win".
"""
def remember(environ, identity):
""" On egress (no challenge required):
args -> [ (header-name, header-value), ...] | None
Return a list of headers suitable for allowing the requesting
system to remember the identification information (e.g. a
Set-Cookie header). Return None if no headers need to be set.
These headers will be appended to any headers returned by the
downstream application.
"""
def forget(environ, identity):
""" On egress (challenge required):
args -> [ (header-name, header-value), ...] | None
Return a list of headers suitable for allowing the requesting
system to forget the identification information (e.g. a
Set-Cookie header with an expires date in the past). Return
None if no headers need to be set. These headers will be
included in the response provided by the challenge app.
"""
class IAuthenticator(IPlugin):
""" On ingress: validate the identity and return a user id or None.
"""
def authenticate(environ, identity):
""" identity -> 'userid' | None
o 'environ' is the WSGI environment.
o 'identity' will be a dictionary (with arbitrary keys and
values).
o The IAuthenticator should return a single user id (optimally
a string) if the identity can be authenticated. If the
identify cannot be authenticated, the IAuthenticator should
return None.
Each instance of a registered IAuthenticator plugin that
matches the request classifier will be called N times during a
single request, where N is the number of identities found by
any IIdentifierPlugin instances.
An authenticator must not raise an exception if it is provided
an identity dictionary that it does not understand (e.g. if it
presumes that 'login' and 'password' are keys in the
dictionary, it should check for the existence of these keys
before attempting to do anything; if they don't exist, it
should return None).
"""
class IChallenger(IPlugin):
""" On egress: Conditionally initiate a challenge to the user to
provide credentials.
Only challenge plugins which match one of the the current
response's classifications will be asked to perform a
challenge.
"""
def challenge(environ, status, app_headers, forget_headers):
""" args -> WSGI application or None
o 'environ' is the WSGI environment.
o 'status' is the status written into start_response by the
downstream application.
o 'app_headers' is the headers list written into start_response by the
downstream application.
o 'forget_headers' is a list of headers which must be passed
back in the response in order to perform credentials reset
(logout). These come from the 'forget' method of
IIdentifier plugin used to do the request's identification.
Examine the values passed in and return a WSGI application
(a callable which accepts environ and start_response as its
two positional arguments, ala PEP 333) which causes a
challenge to be performed. Return None to forego performing a
challenge.
"""
class IMetadataProvider(IPlugin):
"""On ingress: When an identity is authenticated, metadata
providers may scribble on the identity dictionary arbitrarily.
Return values from metadata providers are ignored.
"""
def add_metadata(environ, identity):
"""
Add metadata to the identity (which is a dictionary). One
value is always guaranteed to be in the dictionary when
add_metadata is called: 'repoze.who.userid', representing the
user id of the identity. Availability and composition of
other keys will depend on the identifier plugin which created
the identity.
"""