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