Chris McDonough
2008-03-26 b5a331ef86fd530288f3b955e403e3fc3561d123
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
repoze.who
 
Overview
 
  repoze.who is an identification and authentication framework for
  WSGI.
 
Description
 
  repoze.who's ideas are largely culled from Zope 2's Pluggable
  Authentication Service (PAS) (but repoze.who is not dependent on
  Zope 2 in any way).  It provides no facility for authorization
  (ensuring whether a user can or cannot perform the operation implied
  by the request).  This is considered to be the domain of the WSGI
  application.
 
  It attemtps to reuse implementations from paste.auth for some of its
  functionality.
 
Middleware Responsibilities
 
  repoze.who's middleware has one major function on ingress: it
  conditionally places identification and authentication information
  (including a REMOTE_USER value) into the WSGI environment and allows
  the request to continue to a downstream WSGI application.
 
  repoze.who's middleware has one major function on egress: it
  examines the headers set by the downstream application, the WSGI
  environment, or headers supplied by other plugins and conditionally
  challenges for credentials.
 
PasteDeploy Configuration
 
Classifiers
 
  repoze.who "classifies" the request on middleware ingress.  Request
  classification happens before identification and authentication.  A
  request from a browser might be classified a different way that a
  request from an XML-RPC client.  repoze.who uses request classifiers
  to decide which other components to consult during subsequent
  identification, authentication, and challenge steps.  Plugins are
  free to advertise themselves as willing to participate in
  identification and authorization for a request based on this
  classification.
 
  The classification system is pluggable.  repoze.who provides a
  default classifier that you may use.  You may extend the
  classification system by making repoze.who aware of a different
  classifier implementation.
 
Plugins
 
  repoze.who is designed around the concept of plugins.  Plugins are
  instances that are willing to perform one or more identification-
  and/or authentication-related duties.  When you register a plugin,
  you register a plugin factory, which is a callable that accepts
  configuration parameters.  The callable must return an instance of a
  plugin when called.  Each plugin can be configured arbitrarily using
  values in a repoze.who-specific configuration file.
 
  repoze.who consults the set of configured plugins when it intercepts
  a WSGI request, and gives some subset of them a chance to influence
  what is added to the WSGI environment.
 
Request (Ingress) Stages
 
  repoze.who performs the following operations in the following order
  during middleware ingress:
 
  1.  Request Classification
 
      The WSGI environment is examined and the request is classified
      into one "type" of request.  The callable named as
      'request_classifer=' within the '[general]' section is used to
      perform the classification.  It returns a value that is
      considered to be the request classification.
 
  2.  Identification
 
      Identifiers which nominate themselves as willing to extract data
      for a particular class of request (as provided by the request
      classifier) will be consulted to retrieve credentials data from
      the environment.  For example, a basic auth identifier might use
      the HTTP_AUTHORIZATION header to find login and password
      information.  Identifiers are also responsible for providing
      header information to set and remove authentication information
      in the response.
 
  3.  Authentication
 
      Authenticators which nominate themselves as willing to
      authenticate for a particular class of request will be consulted
      to compare information provided by the identification plugins
      that returned credentials.  For example, an htpasswd
      authenticator might look in a file for a user record matching
      any of the identities.  If it finds one, and if the password
      listed in the record matches the password provided by an
      identity, the userid of the user would be returned (which would
      be the same as the login name).
 
  4.  Metadata Provision
 
      The identity of the authenticated user found during the
      authentication step can be augmented with arbitrary metadata.
      For example, a metadata provider plugin might augment the
      identity with first, middle and last names, or a more
      specialized metadata provider might augment the identity with a
      list of role or group names.
 
Response (Egress) Stages
 
  repoze.who performs the following operations in the following order
  during middleware egress:
 
  1.  Challenge Decision
 
      The WSGI environment and the status and headers returned by the
      downstream application may be examined to determine whether a
      challenge is required.  Typically, only the status is used (if
      it starts with "401 ", a challenge is required).  This behavior
      is pluggable.
 
  2.  Challenge
 
      Challengers which nominate themselves as willing to execute a
      challenge for a particular class of request (as provided by the
      classifier) will be consulted, and one will be chosen to perform
      a challenge.  A challenger plugin can use application-returned
      headers, the WSGI environment, and other items to determine what
      sort of operation should be performed to actuate the challenge.
      Note that repoze.who defers to the identifier plugin which
      provided the identity (if any) to reset credentials at challenge
      time; this is not the responsibility of the challenger.
 
Plugin Types
 
  Identifier Plugins
 
    You can register a plugin as willing to act as an "identifier".
    An identifier examines the WSGI environment and attempts to
    extract credentials from the environment.  These credentials are
    used by authenticator plugins to perform authentication.  In some
    cases, an identification plugin can "preauthenticate" an identity
    (and can thus act as an authenticator plugin).
 
  Authenticator Plugins
 
    You may register a plugin as willing to act as an "authenticator".
    Authenticator plugins are responsible for resolving a set of
    credentials provided by an identifier plugin into a user id.
    Typically, authenticator plugins will perform a lookup into a
    database or some other persistent store, check the provided
    credentials against the stored data, and return a user id if the
    credentials can be validated.
 
    The user id provided by an authenticator is eventually passed to
    downstream WSGI applications in the "REMOTE_USER' environment
    variable.  Additionally, the "identity" of the user (as provided
    by the identifier from whence the identity came) is passed along
    to downstream application in the 'repoze.who.identity' environment
    variable.
 
  Metadata Provider Plugins
 
    You may register a plugin as willing to act as a "metadata
    provider" (aka mdprovider).  Metadata provider plugins are
    responsible for adding arbitrary information to the identity
    dictionary for consumption by downstream applications.
 
  Challenger Plugins
 
    You may register a plugin as willing to act as a "challenger".
    Challenger plugins are responsible for initiating a challenge to
    the requesting user.  Challenger plugins are invoked by repoze.who
    when it decides a challenge is necessary. A challenge might
    consist of displaying a form or presenting the user with a basic
    or digest authentication dialog.
 
Configuration File Example
 
  repoze.who is configured using a ConfigParser-style .INI file.  The
  configuration file has five main types of sections: plugin sections,
  a general section, an identifiers section, an authenticators section,
  and a challengers section.  Each "plugin" section defines a
  configuration for a particular plugin.  The identifiers,
  authenticators, and challengers sections refer to these plugins to
  form a site configuration.  The general section is general middleware
  configuration.
 
Example repoze.who Configuration File (*NOTE: SCIENCE FICTION, not yet
implemented!*)
 
  repoze.who is designed to be used within a PasteDeploy configuration
  file:
 
    [filter:who]
    use = egg:repoze.who#who
    config_file = %(here)s/who.ini
 
  Below is an example of a configuration file that might be used to
  configure the repoze.who middleware.  A set of plugins are defined,
  and they are referred to by following non-plugin sections.
 
  In the below configuration, five plugins are defined.  The form, and
  basicauth plugins are nominated to act as challenger plugins.  The
  form, cookie, and basicauth plugins are nominated to act as
  identification plugins.  The htpasswd and sqlusers plugins are
  nominated to act as authenticator plugins.
 
    [plugin:form]
    # identificaion and challenge
    use = egg:repoze.who#form
    login_form_qs = __do_login
    rememberer_name = cookie
    form = %(here)s/login_form.html
 
    [plugin:cookie]
    # identification
    use = egg:repoze.who#cookie
    cookie_name = repoze.who.auth
 
    [plugin:basicauth]
    # identification and challenge
    use = egg:repoze.who#basicauth
    realm = repoze
 
    [plugin:htpasswd]
    # authentication
    use = egg:repoze.who#htpasswd
    filename = %(here)s/users.htpasswd
    check_fn = egg:repoze.who#crypt_check
 
    [plugin:sqlusers]
    # authentication
    use = egg:repoze.who#squsersource
    db = sqlite://database?user=foo&pass=bar
    get_userinfo = select id, password from users
    check_fn = egg:repoze.who#crypt_check
 
    [plugin:properties]
    use = egg:repoze.who#ini_metadata
    filename = %(here)s/etc/properties.ini
    handler = egg:repoze.who#ini_default
 
    [plugin:roles]
    use = egg:repoze.who#ini_metadata
    filename = %(here)s/etc/roles.ini
 
    [general]
    request_classifier = egg:repoze.who#defaultrequestclassifier
    challenge_decider = egg:repoze.who#defaultchallengedecider
 
    [identifiers]
    # plugin_name:classifier_name:.. or just plugin_name (good for any)
    plugins =
          form:browser
          basicauth
 
    [authenticators]
    # plugin_name:classifier_name.. or just plugin_name (good for any)
    plugins =
          htpasswd
          sqlusers
 
    [challengers]
    # plugin_name:classifier_name:.. or just plugin_name (good for any)
    plugins =
          form:browser
          basicauth
 
    [mdproviders]
    plugins =
          properties
          roles
 
Further Description of Example Config
 
  The basicauth section configures a plugin that does identification
  and challenge for basic auth credentials.  The form section
  configures a plugin that does identification and challenge (its
  implementation defers to the cookie plugin for identification
  "forget" and "remember" duties, thus the "identifier_impl_name" key;
  this is looked up at runtime).  The cookie section configures a
  plugin that does identification for cookie auth credentials.  The
  htpasswd plugin obtains its user info from a file.  The sqlusers
  plugin obtains its user info from a sqlite database.
 
  The identifiers section provides an ordered list of plugins that are
  willing to provide identification capability.  These will be
  consulted in the defined order.  The tokens on each line of the
  'plugins=' key are in the form
  "plugin_name:requestclassifier_name:..."  (or just "plugin_name" if
  the plugin can be consulted regardless of the classification of the
  request).  The configuration above indicates that the system will
  look for credentials using the form plugin (if the request is
  classified as a browser request), then the cookie identifier
  (unconditionally), then the basic auth plugin (unconditionally).
 
  The authenticators section provides an ordered list of plugins that
  provide authenticator capability.  These will be consulted in the
  defined order, so the system will look for users in the file, then
  in the sql database when attempting to validate credentials.  No
  classification prefixes are given to restrict which of the two
  plugins are used, so both plugins are consulted regardless of the
  classification of the request.  Each authenticator is called with
  each set of identities found by the identifier plugins.  The first
  identity that can be authenticated is used to set "REMOTE_USER".
 
  The mdproviders section provides an ordered list of plugins that
  provide metadata provider capability.  These will be consulted in
  the defined order.  Each will have a chance (on ingress) to provide
  add metadata to the authenticated identity.  Our example mdproviders
  section shows two plugins configured: "properties", and "roles".
  The (fictional) properties plugin will add information related to
  user properties (e.g. first name, last name, phone number, etc) to
  the identity dictionary.  The (fictional) roles mdprovider will add
  information representing the user's "roles" in the context of the
  current request to the identity dictionary.
 
  The challengers section provides an ordered list of plugins that
  provide challenger capability.  These will be consulted in the
  defined order, so the system will consult the cookie auth plugin
  first, then the basic auth plugin.  Each will have a chance to
  initiate a challenge.  The above configuration indicates that the
  form challenger will fire if it's a browser request, and the basic
  auth challenger will fire if it's not (fallback).
 
Writing An Identifier Plugin
 
  An identifier plugin (aka an IIdentifier plugin) must do three
  things: extract credentials from the request and turn them into an
  "identity", "remember" credentials, and "forget" credentials.
 
  Here's a simple cookie identification plugin that does these three
  things::
 
    class InsecureCookiePlugin(object):
 
        def __init__(self, cookie_name):
            self.cookie_name = cookie_name
 
        def identify(self, environ):
            cookies = get_cookies(environ)
            cookie = cookies.get(self.cookie_name)
 
            if cookie is None:
                return None
 
            import binascii
            try:
                auth = cookie.value.decode('base64')
            except binascii.Error: # can't decode
                return None
 
            try:
                login, password = auth.split(':', 1)
                return {'login':login, 'password':password}
            except ValueError: # not enough values to unpack
                return None
 
        def remember(self, environ, identity):
            cookie_value = '%(login)s:%(password)s' % identity
            cookie_value = cookie_value.encode('base64').rstrip()
            from paste.request import get_cookies
            cookies = get_cookies(environ)
            existing = cookies.get(self.cookie_name)
            value = getattr(existing, 'value', None)
            if value != cookie_value:
                # return a Set-Cookie header
                set_cookie = '%s=%s; Path=/;' % (self.cookie_name, cookie_value)
                return [('Set-Cookie', set_cookie)]
 
        def forget(self, environ, identity):
            # return a expires Set-Cookie header
            expired = ('%s=""; Path=/; Expires=Sun, 10-May-1971 11:59:00 GMT' %
                       self.cookie_name)
            return [('Set-Cookie', expired)]
        
        def __repr__(self):
            return '<%s %s>' % (self.__class__.__name__, id(self))
 
  Note that the plugin implements three "interface" methods:
  "identify", "forget" and "remember".  The formal specification for
  the arguments and return values expected from these methods are
  available in the "interfaces.py" file in repoze.who as the
  'IIdentifier' interface, but let's examine them less formally one at
  a time.
 
  identify(environ) --
 
    The 'identify' method of our InsecureCookiePlugin accepts a single
    argument "environ".  This will be the WSGI environment dictionary.
    Our plugin attempts to grub through the cookies sent by the
    client, trying to find one that matches our cookie name.  If it
    finds one that matches, it attempts to decode it and turn it into
    a login and a password, which it returns as values in a
    dictionary.  This dictionary is thereafter known as an "identity".
    If it finds no credentials in cookies, it returns None (which is
    not considered an identity).
 
    More generally, the 'identify' method of an IIdentifier plugin is
    called once on WSGI request "ingress", and it is expected to grub
    arbitrarily through the WSGI environment looking for credential
    information.  In our above plugin, the credential information is
    expected to be in a cookie but credential information could be in
    a cookie, a form field, basic/digest auth information, a header, a
    WSGI environment variable set by some upstream middleware or
    whatever else someone might use to stash authentication
    information.  If the plugin finds credentials in the request, it's
    expected to return an "identity": this must be a dictionary.  The
    dictionary is not required to have any particular keys or value
    composition, although it's wise if the identification plugin looks
    for both a login name and a password information to return at
    least {'login':login_name, 'password':password}, as some
    authenticator plugins may depend on presence of the names "login"
    and "password" (e.g. the htpasswd and sql IAuthenticator plugins).
    If an IIdentifier plugin finds no credentials, it is expected to
    return None.
 
    An IIdentifier plugin is also permitted to "preauthenticate" an
    identity.  If the identifier plugin knows that the identity is
    "good" (e.g. in the case of ticket-based authentication where the
    userid is embedded into the ticket), it can insert a special key
    into the identity dictionary: 'repoze.who.userid'.  If this key is
    present in the identity dictionary, no authenticators will be
    asked to authenticate the identity.  This effectively allows an
    IIdentifier plugin to become an IAuthenticator plugin when
    breaking apart the responsibility into two separate plugins is
    "make-work".  Preauthenticated identities will be selected first
    when deciding which identity to use for any given request.  Our
    cookie plugin doesn't use this feature.
 
  remember(environ, identity) --
 
    If we've passed a REMOTE_USER to the WSGI application during
    ingress (as a result of providing an identity that could be
    authenticated), and the downstream application doesn't kick back
    with an unauthorized response, on egress we want the requesting
    client to "remember" the identity we provided if there's some way
    to do that and if he hasn't already, in order to ensure he will
    pass it back to us on subsequent requests without requiring
    another login.  The remember method of an IIdentifier plugin is
    called for each non-unauthenticated response.  It is the
    responsibility of the IIdentifier plugin to conditionally return
    HTTP headers that will cause the client to remember the
    credentials implied by "identity".
    
    Our InsecureCookiePlugin implements the "remember" method by
    returning headers which set a cookie if and only if one is not
    already set with the same name and value in the WSGI environment.
    These headers will be tacked on to the response headers provided
    by the downstream application during the response.
 
    When you write a remember method, most of the work involved is
    determining *whether or not* you need to return headers.  It's
    typical to see remember methods that compute an "old state" and a
    "new state" and compare the two against each other in order to
    determine if headers need to be returned.  In our example
    InsecureCookiePlugin, the "old state" is "cookie_value" and the
    "new state" is "value".
 
  forget(environ, identity) --
 
    Eventually the WSGI application we're serving will issue a "401
    Unauthorized" or another status signifying that the request could
    not be authorized.  repoze.who intercepts this status and calls
    IIdentifier plugins asking them to "forget" the credentials
    implied by the identity.  It is the "forget" method's job at this
    point to return HTTP headers that will effectively clear any
    credentials on the requesting client implied by the "identity"
    argument.
 
    Our InsecureCookiePlugin implements the "forget" method by
    returning a header which resets the cookie that was set earlier by
    the remember method to one that expires in the past (on my
    birthday, in fact).  This header will be tacked onto the response
    headers provided by the downstream application.
 
Writing an Authenticator Plugin
 
  An authenticator plugin (aka an IAuthenticator plugin) must do only
  one thing (on "ingress"): accept an identity and check if the
  identity is "good".  If the identity is good, it should return a
  "user id".  This user id may or may not be the same as the "login"
  provided by the user.  An IAuthenticator plugin will be called for
  each identity found during the identification phase (there may be
  multiple identities for a single request, as there may be multiple
  IIdentifier plugins active at any given time), so it may be called
  multiple times in the same request.
 
  Here's a simple authenticator plugin that attempts to match an
  identity against ones defined in an "htpasswd" file that does just
  that::
 
    class SimpleHTPasswdPlugin(object):
 
        def __init__(self, filename):
            self.filename = filename
 
        # IAuthenticatorPlugin
        def authenticate(self, environ, identity):
            try:
                login = identity['login']
                password = identity['password']
            except KeyError:
                return None
 
            f = open(self.filename, 'r')
 
            for line in f:
                try:
                    username, hashed = line.rstrip().split(':', 1)
                except ValueError:
                    continue
                if username == login:
                    if crypt_check(password, hashed):
                        return username
            return None
 
    def crypt_check(password, hashed):
        from crypt import crypt
        salt = hashed[:2]
        return hashed == crypt(password, salt)
 
  Note that the plugin implements a single "interface" method:
  "authenticate".  The formal specification for the arguments and
  return values expected from this method is available in the
  "interfaces.py" file in repoze.who as the 'IAuthenticator'
  interface, but we can explore this a little further here.
 
  The 'authenticate' method accepts two arguments: the WSGI
  environment and an identity.  Our SimpleHTPasswdPlugin
  'authenticate' implementation grabs the login and password out of
  the identity and attempts to find the login in the htpasswd file.
  If it finds it, it compares the crypted version of the password
  provided by the user to the crypted version stored in the htpasswd
  file, and finally, if they match, it returns the login.  If they do
  not match, it returns None.
 
  Note that our plugin does not assume that the keys 'login' or
  'password' exist in the identity; although it requires them to do
  "real work" it returns None if they are not present instead of
  raising an exception.  This is required by the IAuthenticator
  interface specification.
 
Writing a Challenger Plugin
 
  A challenger plugin (aka an IChallenger plugin) must do only one
  thing on "egress": return a WSGI application which performs a
  "challenge".  A WSGI application is a callable that accepts an
  "environ" and a "start_response" as its parameters; see "PEP 333"
  for further definition of what a WSGI application is.  A challenge
  asks the user for credentials.
 
  Here's an example of a simple challenger plugin::
 
    from paste.httpheaders import WWW_AUTHENTICATE
    from paste.httpexceptions import HTTPUnauthorized
 
    class BasicAuthChallengerPlugin(object):
 
        def __init__(self, realm):
            self.realm = realm
 
        # IChallenger
        def challenge(self, environ, status, app_headers, forget_headers):
            head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
            if head[0] not in forget_headers:
                head = head + forget_headers
            return HTTPUnauthorized(headers=head)
 
  Note that the plugin implements a single "interface" method:
  "challenge".  The formal specification for the arguments and return
  values expected from this method is available in the "interfaces.py"
  file in repoze.who as the 'IChallenger' interface.  This method is
  called when repoze.who determines that the application has returned
  an "unauthorized" response (e.g. a 401).  Only one challenger will
  be consulted during "egress" as necessary (the first one to return a
  non-None response).  The challenge method takes environ (the WSGI
  environment), 'status' (the status as set by the downstream
  application), the "app_headers" (headers returned by the
  application), and the "forget_headers" (headers returned by all
  participating IIdentifier plugins whom were asked to "forget" this
  user).
 
  Our BasicAuthChallengerPlugin takes advantage of the fact that the
  HTTPUnauthorized exception imported from paste.httpexceptions can be
  used as a WSGI application.  It first makes sure that we don't
  repeat headers if an identification plugin has already set a
  "WWW-Authenticate" header like ours, then it returns an instance of
  HTTPUnauthorized, passing in merged headers.  This will cause a
  basic authentication dialog to be presented to the user.
 
Writing a Metadata Provider Plugin
 
  A metadata provider plugin (aka an IMetadataProvider plugin) must do
  only one thing (on "ingress"): "scribble" on the identity dictionary
  provided to it when it is called.  An IMetadataProvider plugin will
  be called with the final "best" identity found during the
  authentication phase, or not at all if no "best" identity could be
  authenticated.  Thus, each IMetadataProvider plugin will be called
  exactly zero or one times during a request.
 
  Here's a simple metadata provider plugin that provides "property"
  information from a dictionary::
 
    _DATA = {    'chris': {'first_name':'Chris', 'last_name':'McDonough'} 
                 'whit': {'first_name':'Whit', 'last_name':'Morriss'} 
             }
 
    class SimpleMetadataProvider(object):
        def add_metadata(self, environ, identity):
            userid = identity.get('repoze.who.userid')
            identity.update(_DATA.get(userid))
 
Interfaces
 
  See the module repoze.who.interfaces.