David Tulloh
2016-04-20 d7df42ae13a2a9bfb73a76ed96997dad88a794a9
commit | author | age
d95e97 1 .. _about_plugins:
TS 2
3 About :mod:`repoze.who` Plugins
4 ===============================
5
6 Plugin Types
7 ------------
8
9 Identifier Plugins
10 ++++++++++++++++++
11
12 You can register a plugin as willing to act as an "identifier".  An
13 identifier examines the WSGI environment and attempts to extract
14 credentials from the environment.  These credentials are used by
4a067e 15 authenticator plugins to perform authentication.
TS 16
d95e97 17
TS 18 Authenticator Plugins
19 +++++++++++++++++++++
20
21 You may register a plugin as willing to act as an "authenticator".
22 Authenticator plugins are responsible for resolving a set of
23 credentials provided by an identifier plugin into a user id.
24 Typically, authenticator plugins will perform a lookup into a database
25 or some other persistent store, check the provided credentials against
26 the stored data, and return a user id if the credentials can be
27 validated.
28
29 The user id provided by an authenticator is eventually passed to
30 downstream WSGI applications in the "REMOTE_USER' environment
31 variable.  Additionally, the "identity" of the user (as provided by
32 the identifier from whence the identity came) is passed along to
33 downstream application in the ``repoze.who.identity`` environment
34 variable.
35
4a067e 36
d95e97 37 Metadata Provider Plugins
TS 38 +++++++++++++++++++++++++
39
40 You may register a plugin as willing to act as a "metadata provider"
41 (aka mdprovider).  Metadata provider plugins are responsible for
42 adding arbitrary information to the identity dictionary for
43 consumption by downstream applications.  For instance, a metadata
44 provider plugin may add "group" information to the the identity.
4a067e 45
d95e97 46
TS 47 Challenger Plugins
48 ++++++++++++++++++
49
50 You may register a plugin as willing to act as a "challenger".
51 Challenger plugins are responsible for initiating a challenge to the
52 requesting user.  Challenger plugins are invoked by :mod:`repoze.who` when it
53 decides a challenge is necessary. A challenge might consist of
54 displaying a form or presenting the user with a basic or digest
55 authentication dialog.
4a067e 56
d95e97 57
eb49d5 58 .. _default_plugins:
TS 59
d95e97 60 Default Plugin Implementations
TS 61 ------------------------------
62
63 :mod:`repoze.who` ships with a variety of default plugins that do
64 authentication, identification, challenge and metadata provision.
65
66 .. module:: repoze.who.plugins.auth_tkt
67
68 .. class:: AuthTktCookiePlugin(secret [, cookie_name='auth_tkt' [, secure=False [, include_ip=False]]])
69
ac7dde 70   An :class:`AuthTktCookiePlugin` is an ``IIdentifier`` and ``IAuthenticator``
TS 71   plugin which remembers its identity state in a client-side cookie.
d7df42 72   This plugin uses the ``paste.auth.auth_tkt``"auth ticket" protocol and
DT 73   is compatible with Apache's mod_auth_tkt.
ac7dde 74   It should be instantiated passing a *secret*, which is used to encrypt the
d95e97 75   cookie on the client side and decrypt the cookie on the server side.
TS 76   The cookie name used to store the cookie value can be specified
77   using the *cookie_name* parameter.  If *secure* is False, the cookie
78   will be sent across any HTTP or HTTPS connection; if it is True, the
79   cookie will be sent only across an HTTPS connection.  If
80   *include_ip* is True, the ``REMOTE_ADDR`` of the WSGI environment
81   will be placed in the cookie.
82
ac7dde 83   Normally, using the plugin as an identifier requires also using it as
TS 84   an authenticator.
85
d95e97 86 .. note::
TS 87    Using the *include_ip* setting for public-facing applications may
88    cause problems for some users.  `One study
89    <http://westpoint.ltd.uk/advisories/Paul_Johnston_GSEC.pdf>`_ reports
90    that as many as 3% of users change their IP addresses legitimately
91    during a session.
67b3a7 92    
J 93 .. note::
94    Plugin supports remembering user data in the cookie by saving user dict into ``identity['userdata']``
95    parameter of ``remember`` method. They are sent unencrypted and protected by checksum.
96    Data will then be returned every time by ``identify``. This dict must be compatible with
97    ``urllib.urlencode`` function (``urllib.urlparse.urlencode`` in python 3).
98    Saving keys/values with unicode characters is supported only under python 3.
d95e97 99
d7df42 100 .. note::
DT 101    Plugin supports multiple digest algorithms. It defaults to md5 to match
102    the default for mod_auth_tkt and paste.auth.auth_tkt. However md5 is not
103    recommended as there are viable attacks against the hash. Any algorithm
104    from the hashlib library can be specified, currently only sha256 and sha512
105    are supported by mod_auth_tkt.
106
d95e97 107 .. module:: repoze.who.plugins.basicauth
TS 108
109 .. class:: BasicAuthPlugin(realm)
110
111   A :class:`BasicAuthPlugin` plugin is both an ``IIdentifier`` and
112   ``IChallenger`` plugin that implements the Basic Access
113   Authentication scheme described in :rfc:`2617`.  It looks for
114   credentials within the ``HTTP-Authorization`` header sent by
115   browsers.  It challenges by sending an ``WWW-Authenticate`` header
116   to the browser.  The single argument *realm* indicates the basic
117   auth realm that should be sent in the ``WWW-Authenticate`` header.
118
119 .. module:: repoze.who.plugins.htpasswd
120
121 .. class:: HTPasswdPlugin(filename, check)
122
123   A :class:`HTPasswdPlugin` is an ``IAuthenticator`` implementation
124   which compares identity information against an Apache-style htpasswd
125   file.  The *filename* argument should be an absolute path to the
126   htpasswd file' the *check* argument is a callable which takes two
127   arguments: "password" and "hashed", where the "password" argument is
128   the unencrypted password provided by the identifier plugin, and the
129   hashed value is the value stored in the htpasswd file.  If the
130   hashed value of the password matches the hash, this callable should
131   return True.  A default implementation named ``crypt_check`` is
132   available for use as a check function (on UNIX) as
133   ``repoze.who.plugins.htpasswd:crypt_check``; it assumes the values
134   in the htpasswd file are encrypted with the UNIX ``crypt`` function.
135
ab7f3c 136 .. module:: repoze.who.plugins.redirector
TS 137
138 .. class:: RedirectorPlugin(login_url, came_from_param, reason_param, reason_header)
139
140   A :class:`RedirectorPlugin` is an ``IChallenger`` plugin.
141   It redirects to a configured login URL at egress if a challenge is
142   required .
143   *login_url* is the URL that should be redirected to when a
144   challenge is required.  *came_from_param* is the name of an optional
145   query string parameter:  if configured, the plugin provides the current
146   request URL in the redirected URL's query string, using the supplied
147   parameter name.  *reason_param* is the name of an optional
148   query string parameter:  if configured, and the application supplies
149   a header matching *reason_header* (defaulting to
150   ``X-Authorization-Failure-Reason``), the plugin includes that reason in
151   the query string of the redirected URL, using the supplied parameter name.
152   *reason_header* is an optional parameter overriding the default response
153   header name (``X-Authorization-Failure-Reason``) which
154   the plugin checks to find the application-supplied reason for the challenge.
e75a29 155   *reason_header* cannot be set unless *reason_param* is also set.
ab7f3c 156
d95e97 157 .. module:: repoze.who.plugins.sql
TS 158
159 .. class:: SQLAuthenticatorPlugin(query, conn_factory, compare_fn)
160
161   A :class:`SQLAuthenticatorPlugin` is an ``IAuthenticator``
162   implementation which compares login-password identity information
163   against data in an arbitrary SQL database.  The *query* argument
164   should be a SQL query that returns two columns in a single row
165   considered to be the user id and the password respectively.  The SQL
166   query should contain Python-DBAPI style substitution values for
167   ``%(login)``, e.g. ``SELECT user_id, password FROM users WHERE login
168   = %(login)``.  The *conn_factory* argument should be a callable that
169   returns a DBAPI database connection.  The *compare_fn* argument
170   should be a callable that accepts two arguments: ``cleartext`` and
171   ``stored_password_hash``.  It should compare the hashed version of
172   cleartext and return True if it matches the stored password hash,
173   otherwise it should return False.  A comparison function named
174   ``default_password_compare`` exists in the
175   ``repoze.who.plugins.sql`` module demonstrating this.  The
176   :class:`SQLAuthenticatorPlugin`\'s ``authenticate`` method will
177   return the user id of the user unchanged to :mod:`repoze.who`.
178
179 .. class:: SQLMetadataProviderPlugin(name, query, conn_factory, filter)
180
181   A :class:`SQLMetatadaProviderPlugin` is an ``IMetadataProvider``
182   implementation which adds arbitrary metadata to the identity on
183   ingress using data from an arbitrary SQL database.  The *name*
184   argument should be a string.  It will be used as a key in the
185   identity dictionary.  The *query* argument should be a SQL query
186   that returns arbitrary data from the database in a form that accepts
187   Python-binding style DBAPI arguments.  It should expect that a
188   ``__userid`` value will exist in the dictionary that is bound.  The
189   SQL query should contain Python-DBAPI style substitution values for
190   (at least) ``%(__userid)``, e.g. ``SELECT group FROM groups WHERE
191   user_id = %(__userid)``.  The *conn_factory* argument should be a
192   callable that returns a DBAPI database connection.  The *filter*
193   argument should be a callable that accepts the result of the DBAPI
194   ``fetchall`` based on the SQL query.  It should massage the data
195   into something that will be set in the environment under the *name*
196   key.  
197
198
199 Writing :mod:`repoze.who` Plugins
200 ---------------------------------
201
202 :mod:`repoze.who` can be extended arbitrarily through the creation of
203 plugins.  Plugins are of one of four types: identifier plugins,
204 authenticator plugins, metadata provider plugins, and challenge
205 plugins.
206
4a067e 207
d95e97 208 Writing An Identifier Plugin
TS 209 ++++++++++++++++++++++++++++
210
211 An identifier plugin (aka an ``IIdentifier`` plugin) must do three
212 things: extract credentials from the request and turn them into an
213 "identity", "remember" credentials, and "forget" credentials.
214
215 Here's a simple cookie identification plugin that does these three
216 things ::
217
218     class InsecureCookiePlugin(object):
219
220         def __init__(self, cookie_name):
221             self.cookie_name = cookie_name
222
223         def identify(self, environ):
a79685 224             from paste.request import get_cookies
d95e97 225             cookies = get_cookies(environ)
TS 226             cookie = cookies.get(self.cookie_name)
227
228             if cookie is None:
229                 return None
230
231             import binascii
232             try:
233                 auth = cookie.value.decode('base64')
234             except binascii.Error: # can't decode
235                 return None
236
237             try:
238                 login, password = auth.split(':', 1)
239                 return {'login':login, 'password':password}
240             except ValueError: # not enough values to unpack
241                 return None
242
243         def remember(self, environ, identity):
244             cookie_value = '%(login)s:%(password)s' % identity
245             cookie_value = cookie_value.encode('base64').rstrip()
246             from paste.request import get_cookies
247             cookies = get_cookies(environ)
248             existing = cookies.get(self.cookie_name)
249             value = getattr(existing, 'value', None)
250             if value != cookie_value:
251                 # return a Set-Cookie header
252                 set_cookie = '%s=%s; Path=/;' % (self.cookie_name, cookie_value)
253                 return [('Set-Cookie', set_cookie)]
254
255         def forget(self, environ, identity):
256             # return a expires Set-Cookie header
257             expired = ('%s=""; Path=/; Expires=Sun, 10-May-1971 11:59:00 GMT' %
258                        self.cookie_name)
259             return [('Set-Cookie', expired)]
260         
261         def __repr__(self):
262             return '<%s %s>' % (self.__class__.__name__, id(self))
263
4a067e 264
d95e97 265 .identify
TS 266 ~~~~~~~~~
267
268 The ``identify`` method of our InsecureCookiePlugin accepts a single
269 argument "environ".  This will be the WSGI environment dictionary.
270 Our plugin attempts to grub through the cookies sent by the client,
271 trying to find one that matches our cookie name.  If it finds one that
272 matches, it attempts to decode it and turn it into a login and a
273 password, which it returns as values in a dictionary.  This dictionary
274 is thereafter known as an "identity".  If it finds no credentials in
275 cookies, it returns None (which is not considered an identity).
276
277 More generally, the ``identify`` method of an ``IIdentifier`` plugin
278 is called once on WSGI request "ingress", and it is expected to grub
279 arbitrarily through the WSGI environment looking for credential
280 information.  In our above plugin, the credential information is
281 expected to be in a cookie but credential information could be in a
282 cookie, a form field, basic/digest auth information, a header, a WSGI
283 environment variable set by some upstream middleware or whatever else
284 someone might use to stash authentication information.  If the plugin
285 finds credentials in the request, it's expected to return an
286 "identity": this must be a dictionary.  The dictionary is not required
287 to have any particular keys or value composition, although it's wise
288 if the identification plugin looks for both a login name and a
289 password information to return at least {'login':login_name,
290 'password':password}, as some authenticator plugins may depend on
291 presence of the names "login" and "password" (e.g. the htpasswd and
292 sql ``IAuthenticator`` plugins).  If an ``IIdentifier`` plugin finds
293 no credentials, it is expected to return None.
294
295
296 .remember
297 ~~~~~~~~~
298
299 If we've passed a REMOTE_USER to the WSGI application during ingress
300 (as a result of providing an identity that could be authenticated),
301 and the downstream application doesn't kick back with an unauthorized
302 response, on egress we want the requesting client to "remember" the
303 identity we provided if there's some way to do that and if he hasn't
304 already, in order to ensure he will pass it back to us on subsequent
305 requests without requiring another login.  The remember method of an
306 ``IIdentifier`` plugin is called for each non-unauthenticated
307 response.  It is the responsibility of the ``IIdentifier`` plugin to
308 conditionally return HTTP headers that will cause the client to
309 remember the credentials implied by "identity".
310     
311 Our InsecureCookiePlugin implements the "remember" method by returning
312 headers which set a cookie if and only if one is not already set with
313 the same name and value in the WSGI environment.  These headers will
314 be tacked on to the response headers provided by the downstream
315 application during the response.
316
317 When you write a remember method, most of the work involved is
318 determining *whether or not* you need to return headers.  It's typical
319 to see remember methods that compute an "old state" and a "new state"
320 and compare the two against each other in order to determine if
321 headers need to be returned.  In our example InsecureCookiePlugin, the
322 "old state" is ``cookie_value`` and the "new state" is ``value``.
323
4a067e 324
d95e97 325 .forget
TS 326 ~~~~~~~
327
328 Eventually the WSGI application we're serving will issue a "401
329  Unauthorized" or another status signifying that the request could not
330  be authorized.  :mod:`repoze.who` intercepts this status and calls
331  ``IIdentifier`` plugins asking them to "forget" the credentials
332  implied by the identity.  It is the "forget" method's job at this
333  point to return HTTP headers that will effectively clear any
334  credentials on the requesting client implied by the "identity"
335  argument.
336
337  Our InsecureCookiePlugin implements the "forget" method by returning
338  a header which resets the cookie that was set earlier by the remember
339  method to one that expires in the past (on my birthday, in fact).
340  This header will be tacked onto the response headers provided by the
341  downstream application.
4a067e 342
d95e97 343
TS 344 Writing an Authenticator Plugin
345 +++++++++++++++++++++++++++++++
346
347 An authenticator plugin (aka an ``IAuthenticator`` plugin) must do
348 only one thing (on "ingress"): accept an identity and check if the
349 identity is "good".  If the identity is good, it should return a "user
350 id".  This user id may or may not be the same as the "login" provided
351 by the user.  An ``IAuthenticator`` plugin will be called for each
352 identity found during the identification phase (there may be multiple
353 identities for a single request, as there may be multiple
354 ``IIdentifier`` plugins active at any given time), so it may be called
355 multiple times in the same request.
356
357 Here's a simple authenticator plugin that attempts to match an
358 identity against ones defined in an "htpasswd" file that does just
359 that::
360
361     class SimpleHTPasswdPlugin(object):
362
363         def __init__(self, filename):
364             self.filename = filename
365
366         # IAuthenticatorPlugin
367         def authenticate(self, environ, identity):
368             try:
369                 login = identity['login']
370                 password = identity['password']
371             except KeyError:
372                 return None
373
374             f = open(self.filename, 'r')
375
376             for line in f:
377                 try:
378                     username, hashed = line.rstrip().split(':', 1)
379                 except ValueError:
380                     continue
381                 if username == login:
382                     if crypt_check(password, hashed):
383                         return username
384             return None
385
386     def crypt_check(password, hashed):
387         from crypt import crypt
388         salt = hashed[:2]
389         return hashed == crypt(password, salt)
390
391 An ``IAuthenticator`` plugin implements one "interface" method:
392 "authentictate".  The formal specification for the arguments and
393 return values expected from these methods are available in the
394 ``interfaces.py`` file in :mod:`repoze.who` as the ``IAuthenticator``
395 interface, but let's examine this method here less formally.
396
4a067e 397
d95e97 398 .authenticate
TS 399 ~~~~~~~~~~~~~
400
401 The ``authenticate`` method accepts two arguments: the WSGI
402 environment and an identity.  Our SimpleHTPasswdPlugin
403 ``authenticate`` implementation grabs the login and password out of
404 the identity and attempts to find the login in the htpasswd file.  If
405 it finds it, it compares the crypted version of the password provided
406 by the user to the crypted version stored in the htpasswd file, and
407 finally, if they match, it returns the login.  If they do not match,
408 it returns None.
409
410 .. note::
411
412    Our plugin's ``authenticate`` method does not assume that the keys
413    ``login`` or ``password`` exist in the identity; although it
414    requires them to do "real work" it returns None if they are not
415    present instead of raising an exception.  This is required by the
416    ``IAuthenticator`` interface specification.
4a067e 417
d95e97 418
TS 419 Writing a Challenger Plugin
420 +++++++++++++++++++++++++++
421
422 A challenger plugin (aka an ``IChallenger`` plugin) must do only one
423 thing on "egress": return a WSGI application which performs a
424 "challenge".  A WSGI application is a callable that accepts an
425 "environ" and a "start_response" as its parameters; see "PEP 333" for
426 further definition of what a WSGI application is.  A challenge asks
427 the user for credentials.
428
429 Here's an example of a simple challenger plugin::
430
431     from paste.httpheaders import WWW_AUTHENTICATE
432     from paste.httpexceptions import HTTPUnauthorized
433
434     class BasicAuthChallengerPlugin(object):
435
436         def __init__(self, realm):
437             self.realm = realm
438
439         # IChallenger
440         def challenge(self, environ, status, app_headers, forget_headers):
441             head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
442             if head[0] not in forget_headers:
443                 head = head + forget_headers
444             return HTTPUnauthorized(headers=head)
445
446 Note that the plugin implements a single "interface" method:
447 "challenge".  The formal specification for the arguments and return
448 values expected from this method is available in the "interfaces.py"
449 file in :mod:`repoze.who` as the ``IChallenger`` interface.  This method
450 is called when :mod:`repoze.who` determines that the application has
451 returned an "unauthorized" response (e.g. a 401).  Only one challenger
452 will be consulted during "egress" as necessary (the first one to
453 return a non-None response).
454
4a067e 455
d95e97 456 .challenge
TS 457 ~~~~~~~~~~
458
459 The challenge method takes environ (the WSGI environment), 'status'
460 (the status as set by the downstream application), the "app_headers"
461 (headers returned by the application), and the "forget_headers"
462 (headers returned by all participating ``IIdentifier`` plugins whom
463 were asked to "forget" this user).
464
465 Our BasicAuthChallengerPlugin takes advantage of the fact that the
466 HTTPUnauthorized exception imported from paste.httpexceptions can be
467 used as a WSGI application.  It first makes sure that we don't repeat
468 headers if an identification plugin has already set a
469 "WWW-Authenticate" header like ours, then it returns an instance of
470 HTTPUnauthorized, passing in merged headers.  This will cause a basic
471 authentication dialog to be presented to the user.
4a067e 472
d95e97 473
TS 474 Writing a Metadata Provider Plugin
475 ++++++++++++++++++++++++++++++++++
476
477 A metadata provider plugin (aka an ``IMetadataProvider`` plugin) must
478 do only one thing (on "ingress"): "scribble" on the identity
479 dictionary provided to it when it is called.  An ``IMetadataProvider``
480 plugin will be called with the final "best" identity found during the
481 authentication phase, or not at all if no "best" identity could be
482 authenticated.  Thus, each ``IMetadataProvider`` plugin will be called
483 exactly zero or one times during a request.
484
485 Here's a simple metadata provider plugin that provides "property"
486 information from a dictionary::
487
488     _DATA = {    
489         'chris': {'first_name':'Chris', 'last_name':'McDonough'} ,
490         'whit': {'first_name':'Whit', 'last_name':'Morriss'} 
491         }
492
493     class SimpleMetadataProvider(object):
494
495         def add_metadata(self, environ, identity):
496             userid = identity.get('repoze.who.userid')
497             info = _DATA.get(userid)
498             if info is not None:
499                 identity.update(info)
500
4a067e 501
d95e97 502 .add_metadata
TS 503 ~~~~~~~~~~~~~
504
505 Arbitrarily add information to the identity dict based in other data
506 in the environment or identity.  Our plugin adds ``first_name`` and
507 ``last_name`` values to the identity if the userid matches ``chris``
508 or ``whit``.
eb49d5 509
TS 510
511 Known Plugins for :mod:`repoze.who`
512 ===================================
513
514
515 Plugins shipped with :mod:`repoze.who`
516 --------------------------------------
517
518 See :ref:`default_plugins`.
519
520
a446d6 521 Deprecated plugins
TS 522 ------------------
523
524 The :mod:`repoze.who.deprecatedplugins` distribution bundles the following
525 plugin implementations which were shipped with :mod:`repoze.who` prior
526 to version 2.0a3.  These plugins are deprecated, and should only be used
527 while migrating an existing deployment to replacement versions.
528
529 :class:`repoze.who.plugins.cookie.InsecureCookiePlugin`
530   An ``IIdentifier`` plugin which stores identification information in an
531   insecure form (the base64 value of the username and password separated by
532   a colon) in a client-side cookie.  Please use the
533   :class:`AuthTktCookiePlugin` instead.
534
535 :class:`repoze.who.plugins.form.FormPlugin`
536
537   An ``IIdentifier`` and ``IChallenger`` plugin,  which intercepts form POSTs
538   to gather identification at ingress and conditionally displays a login form
539   at egress if challenge is required.
540   
541   Applications should supply their
542   own login form, and use :class:`repoze.who.api.API` to authenticate
543   and remember users.  To replace the challenger role, please use
544   :class:`repoze.who.plugins.redirector.RedirectorPlugin`, configured with
545   the URL of your application's login form.
546
547 :class:`repoze.who.plugins.form.RedirectingFormPlugin`
548
549   An ``IIdentifier`` and ``IChallenger`` plugin, which intercepts form POSTs
550   to gather identification at ingress and conditionally redirects a login form
551   at egress if challenge is required.
552   
553   Applications should supply their
554   own login form, and use :class:`repoze.who.api.API` to authenticate
555   and remember users.  To replace the challenger role, please use
556   :class:`repoze.who.plugins.redirector.RedirectorPlugin`, configured with
557   the URL of your application's login form.
558
559
eb49d5 560 Third-party Plugins
TS 561 -------------------
562
563 :class:`repoze.who.plugins.zodb.ZODBPlugin`
564     This class implements the :class:`repoze.who.interfaces.IAuthenticator`
565     and :class:`repoze.who.interfaces.IMetadataProvider` plugin interfaces
566     using ZODB database lookups.  See
567     http://pypi.python.org/pypi/repoze.whoplugins.zodb/
568
569 :class:`repoze.who.plugins.ldap.LDAPAuthenticatorPlugin`
570     This class implements the :class:`repoze.who.interfaces.IAuthenticator`
571     plugin interface using the :mod:`python-ldap` library to query an LDAP
572     database.  See http://code.gustavonarea.net/repoze.who.plugins.ldap/
573
574 :class:`repoze.who.plugins.ldap.LDAPAttributesPlugin`
575     This class implements the :class:`repoze.who.interfaces.IMetadataProvider`
576     plugin interface using the :mod:`python-ldap` library to query an LDAP
577     database.  See http://code.gustavonarea.net/repoze.who.plugins.ldap/
578
579 :class:`repoze.who.plugins.friendlyform.FriendlyFormPlugin`
580     This class implements the :class:`repoze.who.interfaces.IIdentifier` and 
581     :class:`repoze.who.interfaces.IChallenger` plugin interfaces.  It is
582     similar to :class:`repoze.who.plugins.form.RedirectingFormPlugin`,
583     bt with with additional features:
584
585     - Users are not challenged on logout, unless the referrer URL is a
586       private one (but that’s up to the application).
587
588     - Developers may define post-login and/or post-logout pages.
589
590     - In the login URL, the amount of failed logins is available in the
591       environ. It’s also increased by one on every login try. This counter
592       will allow developers not using a post-login page to handle logins that
593       fail/succeed.
594
595     See http://code.gustavonarea.net/repoze.who-friendlyform/ 
596
597 :func:`repoze.who.plugins.openid.identifiers.OpenIdIdentificationPlugin`
598     This class implements the :class:`repoze.who.interfaces.IIdentifier`,
599     :class:`repoze.who.interfaces.IAuthenticator`, and 
600     :class:`repoze.who.interfaces.IChallenger` plugin interfaces using OpenId.
601     See http://quantumcore.org/docs/repoze.who.plugins.openid/
602
603 :func:`repoze.who.plugins.openid.classifiers.openid_challenge_decider`
604     This function provides the :class:`repoze.who.interfaces.IChallengeDecider`
605     interface using OpenId.  See
606     http://quantumcore.org/docs/repoze.who.plugins.openid/
607
608 :class:`repoze.who.plugins.use_beaker.UseBeakerPlugin`
609     This packkage provids a :class:`repoze.who.interfaces.IIdentifier` plugin
610     using :mod:`beaker.session` cache.  See
611     http://pypi.python.org/pypi/repoze.who-use_beaker/
612
613 :class:`repoze.who.plugins.cas.main_plugin.CASChallengePlugin`
614     This class implements the :class:`repoze.who.interfaces.IIdentifier`
615     :class:`repoze.who.interfaces.IAuthenticator`, and 
616     :class:`repoze.who.interfaces.IChallenger` plugin interfaces using CAS.
617     See http://pypi.python.org/pypi/repoze.who.plugins.cas
618
619 :class:`repoze.who.plugins.cas.challenge_decider.my_challenge_decider`
620     This function provides the :class:`repoze.who.interfaces.IChallengeDecider`
621     interface using CAS.  See
622     http://pypi.python.org/pypi/repoze.who.plugins.cas/
623
624 :class:`repoze.who.plugins.recaptcha.captcha.RecaptchaPlugin`
625     This class implements the :class:`repoze.who.interfaces.IAuthenticator`
626     plugin interface, using the recaptch API.
627     See http://pypi.python.org/pypi/repoze.who.plugins.recaptcha/
628
629 :class:`repoze.who.plugins.sa.SQLAlchemyUserChecker`
630     User existence checker for
631     :class:`repoze.who.plugins.auth_tkt.AuthTktCookiePlugin`, based on
632     the SQLAlchemy ORM. See http://pypi.python.org/pypi/repoze.who.plugins.sa/
633
634 :class:`repoze.who.plugins.sa.SQLAlchemyAuthenticatorPlugin`
635     This class implements the :class:`repoze.who.interfaces.IAuthenticator`
636     plugin interface, using the the SQLAlchemy ORM.
637     See http://pypi.python.org/pypi/repoze.who.plugins.sa/
638     
639 :class:`repoze.who.plugins.sa.SQLAlchemyUserMDPlugin`
640     This class implements the :class:`repoze.who.interfaces.IMetadataProvider`
641     plugin interface, using the the SQLAlchemy ORM.
642     See http://pypi.python.org/pypi/repoze.who.plugins.sa/
643
644 :class:`repoze.who.plugins.formcookie.CookieRedirectingFormPlugin`
645     This class implements the :class:`repoze.who.interfaces.IIdentifier` and 
646     :class:`repoze.who.interfaces.IChallenger` plugin interfaces, similar
647     to :class:`repoze.who.plugins.form.RedirectingFormPlugin`.  The
648     plugin tracks the ``came_from`` URL via a cookie, rather than the query
649     string.  See http://pypi.python.org/pypi/repoze.who.plugins.formcookie/