Repository: falconry/falcon

Issue:
Add support for setting the realm to the HTTPUnauthorized class
Currently the class recognizes a _scheme_ kwarg that can include a realm string, but IMO it would be better to make this explicit. 

See also RFC 7235.



Discussion:
I can work on this.

@kgriffs it looks like the RFC says you can have multiple comma-separated challenges in a single header, or multiple headers. Each challenge is a scheme and zero or more comma separated key=value pairs. Also, optionally, the key=value pairs could be base64 encoded.

In order to implement all of this, I suggest the following:
- Add a new kwarg for accepting a list of valid challenges. Each challenge would need a "scheme" item, and the other items would be used in comma-separate key=value format.
- Add a new kwarg to control whether to base64 encode the key=value options
- Add a new kwarg to control whether to set a single or multiple WWW-Authenticate parameters

I'm not sure whether this is the best way to implement though, so any feedback before I dive in would be appreciated. I'm also not sure what backwards-compatibility would look like, or if you want to drop it since this is a future milestone feature.

@mcmasterathl  Sounds good. I think someone else started work on this, and was taking the approach of just taking a list of opaque challenge strings. So it just leaves the formatting of each challenge to the caller. This is probably OK for 0.3, but we may want to do something more sophisticated (as recommended above) for 0.4 or 0.5. 

Let's see if a PR comes in for this and then we may want to create an additional issue to look at doing more, as previously mentioned.

hey Kurt, i will try and send a PR by today. After that you can let me know if you need anymore changes on that.


Resolving patch:
diff --git a/falcon/errors.py b/falcon/errors.py
--- a/falcon/errors.py
+++ b/falcon/errors.py
@@ -49,18 +49,18 @@ class HTTPUnauthorized(HTTPError):
         title (str): Error title (e.g., 'Authentication Required').
         description (str): Human-friendly description of the error, along with
             a helpful suggestion or two.
-        scheme (str): Authentication scheme to use as the value of the
-            WWW-Authenticate header in the response (default ``None``).
+        challenges (iterable of str): One or more authentication
+            challenges to use as the value of the WWW-Authenticate header in
+            the response.
         kwargs (optional): Same as for ``HTTPError``.
 
     """
 
-    def __init__(self, title, description, **kwargs):
+    def __init__(self, title, description, challenges, **kwargs):
         headers = kwargs.setdefault('headers', {})
 
-        scheme = kwargs.pop('scheme', None)
-        if scheme is not None:
-            headers['WWW-Authenticate'] = scheme
+        if challenges:
+            headers['WWW-Authenticate'] = ', '.join(challenges)
 
         HTTPError.__init__(self, status.HTTP_401, title, description, **kwargs)
 


Test patch:
diff --git a/tests/test_httperror.py b/tests/test_httperror.py
--- a/tests/test_httperror.py
+++ b/tests/test_httperror.py
@@ -74,14 +74,17 @@ class UnauthorizedResource:
     def on_get(self, req, resp):
         raise falcon.HTTPUnauthorized('Authentication Required',
                                       'Missing or invalid token header.',
-                                      scheme='Token; UUID')
+                                      ['Basic realm="simple"'])
 
+    def on_post(self, req, resp):
+        raise falcon.HTTPUnauthorized('Authentication Required',
+                                      'Missing or invalid token header.',
+                                      ['Newauth realm="apps"',
+                                       'Basic realm="simple"'])
 
-class UnauthorizedResourceSchemaless:
-
-    def on_get(self, req, resp):
+    def on_put(self, req, resp):
         raise falcon.HTTPUnauthorized('Authentication Required',
-                                      'Missing or invalid token header.')
+                                      'Missing or invalid token header.', [])
 
 
 class NotFoundResource:
@@ -454,15 +457,20 @@ def test_401(self):
         self.simulate_request('/401')
 
         self.assertEqual(self.srmock.status, falcon.HTTP_401)
-        self.assertIn(('www-authenticate', 'Token; UUID'),
+        self.assertIn(('www-authenticate', 'Basic realm="simple"'),
                       self.srmock.headers)
 
-    def test_401_schemaless(self):
-        self.api.add_route('/401', UnauthorizedResourceSchemaless())
-        self.simulate_request('/401')
+        self.simulate_request('/401', method='POST')
+
+        self.assertEqual(self.srmock.status, falcon.HTTP_401)
+        self.assertIn(('www-authenticate', 'Newauth realm="apps", '
+                      'Basic realm="simple"'),
+                      self.srmock.headers)
+
+        self.simulate_request('/401', method='PUT')
 
         self.assertEqual(self.srmock.status, falcon.HTTP_401)
-        self.assertNotIn(('www-authenticate', 'Token'), self.srmock.headers)
+        self.assertNotIn(('www-authenticate', []), self.srmock.headers)
 
     def test_404_without_body(self):
         self.api.add_route('/404', NotFoundResource())