Repository: pallets/werkzeug

Issue:
Unauthorized HTTPException can't take a WWW-Authenticate header
Per spec, a 401 Unauthorized response [must send a WWW-Authenticate challenge header](http://tools.ietf.org/html/rfc7235#section-3.1).

Should it be modified to take a list of `werkzeug.datastructure.WWWAuthenticate` and serialise them to headers in get_headers?



Discussion:

Resolving patch:
diff --git a/werkzeug/exceptions.py b/werkzeug/exceptions.py
--- a/werkzeug/exceptions.py
+++ b/werkzeug/exceptions.py
@@ -213,19 +213,45 @@ class BadHost(BadRequest):
 
 class Unauthorized(HTTPException):
 
-    """*401* `Unauthorized`
+    """*401* ``Unauthorized``
 
-    Raise if the user is not authorized.  Also used if you want to use HTTP
-    basic auth.
+    Raise if the user is not authorized to access a resource.
+
+    The ``www_authenticate`` argument should be used to set the
+    ``WWW-Authenticate`` header. This is used for HTTP basic auth and
+    other schemes. Use :class:`~werkzeug.datastructures.WWWAuthenticate`
+    to create correctly formatted values. Strictly speaking a 401
+    response is invalid if it doesn't provide at least one value for
+    this header, although real clients typically don't care.
+
+    :param www-authenticate: A single value, or list of values, for the
+        WWW-Authenticate header.
+    :param description: Override the default message used for the body
+        of the response.
     """
     code = 401
     description = (
-        'The server could not verify that you are authorized to access '
-        'the URL requested.  You either supplied the wrong credentials (e.g. '
-        'a bad password), or your browser doesn\'t understand how to supply '
-        'the credentials required.'
+        'The server could not verify that you are authorized to access'
+        ' the URL requested. You either supplied the wrong credentials'
+        " (e.g. a bad password), or your browser doesn't understand"
+        ' how to supply the credentials required.'
     )
 
+    def __init__(self, www_authenticate=None, description=None):
+        HTTPException.__init__(self, description)
+        if not isinstance(www_authenticate, (tuple, list)):
+            www_authenticate = (www_authenticate,)
+        self.www_authenticate = www_authenticate
+
+    def get_headers(self, environ=None):
+        headers = HTTPException.get_headers(self, environ)
+        if self.www_authenticate:
+            headers.append((
+                'WWW-Authenticate',
+                ', '.join([str(x) for x in self.www_authenticate])
+            ))
+        return headers
+
 
 class Forbidden(HTTPException):
 


Test patch:
diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py
--- a/tests/test_exceptions.py
+++ b/tests/test_exceptions.py
@@ -15,6 +15,7 @@
 import pytest
 
 from werkzeug import exceptions
+from werkzeug.datastructures import WWWAuthenticate
 from werkzeug.wrappers import Response
 from werkzeug._compat import text_type
 
@@ -30,7 +31,7 @@ def test_proxy_exception():
 
 @pytest.mark.parametrize('test', [
     (exceptions.BadRequest, 400),
-    (exceptions.Unauthorized, 401),
+    (exceptions.Unauthorized, 401, 'Basic "test realm"'),
     (exceptions.Forbidden, 403),
     (exceptions.NotFound, 404),
     (exceptions.MethodNotAllowed, 405, ['GET', 'HEAD']),
@@ -85,8 +86,23 @@ def test_exception_repr():
     assert repr(exc) == "<HTTPException '???: Unknown Error'>"
 
 
-def test_special_exceptions():
+def test_method_not_allowed_methods():
     exc = exceptions.MethodNotAllowed(['GET', 'HEAD', 'POST'])
     h = dict(exc.get_headers({}))
     assert h['Allow'] == 'GET, HEAD, POST'
     assert 'The method is not allowed' in exc.get_description()
+
+
+def test_unauthorized_www_authenticate():
+    basic = WWWAuthenticate()
+    basic.set_basic("test")
+    digest = WWWAuthenticate()
+    digest.set_digest("test", "test")
+
+    exc = exceptions.Unauthorized(www_authenticate=basic)
+    h = dict(exc.get_headers({}))
+    assert h['WWW-Authenticate'] == str(basic)
+
+    exc = exceptions.Unauthorized(www_authenticate=[digest, basic])
+    h = dict(exc.get_headers({}))
+    assert h['WWW-Authenticate'] == ', '.join((str(digest), str(basic)))