Headers disappear when added to Unauthorized exception

Ok the aim here is to the kerberos/spnego handshake between the browser (basic vue.js app) and the sanic backend.

the handshake should be that:

  1. browser hits the site (/about in my case)
  2. sanic should reply with 401 and header www-authenticate: negotiate
  3. vue/browser should then send the user’s cached creds with header Authorize

the best thing i can find for (2) is the Unauthorized exception.
But for some reason, the configured www-authenticate header is stripped before sending.

I’ve tried following the code progression through the sanic package and i can see the www-authenticate header being added to the exception object, but then when the exception is finally raised it never sends (checked both in browser tools and wireshark to be sure).

server.py looks like this:

from sanic import Sanic
import sanic.response
from sanic.response import text
from sanic_jwt import Initialize, Claim

from classviews.auth.view import authenticate
from classviews.auth import view
from classviews.createtoken.view import Create_Token
from classviews.simpleasyncview.view import SimpleAsyncView
from classviews.simpleview.view import SimpleView

from sanic_ext import Extend


from sanic.log import logger, logging


# add security groups list to token as a claim
class GroupsClaim(Claim):
    key = 'groups'

    def setup(self, payload, user):
        return user.groups

    def verify(self, value):
        return value == 'bar'


app = Sanic('some_name')

app.config.CORS_ORIGINS = "http://localhost,http://127.0.0.1/"
app.config.CORS_SUPPORTS_CREDENTIALS = "true" #access-control-allow-credentials
app.config.CORS_ALLOW_HEADERS = ['content-type','authorization','www-authenticate'] #access-control-allow-headers
Extend(app)

Initialize(app, authenticate=authenticate, custom_claims=[GroupsClaim])

app.add_route(SimpleView.as_view(), '/')
app.add_route(SimpleAsyncView.as_view(), '/async')
app.add_route(Create_Token.as_view(), '/createtoken')


if __name__ == '__main__':
    app.run(host="0.0.0.0", port=7777, debug=True, access_log=True)

and classviews.auth.view looks like this:

from sanic.views import HTTPMethodView
from sanic_jwt import Initialize
from sanic_jwt import exceptions
from sanic import response, text, empty
from sanic.exceptions import Unauthorized, NotFound


class User:

    def __init__(self, id, username, password, groups):
        self.user_id = id
        self.username = username
        self.password = password
        self.groups = groups

    def __repr__(self):
        return "User(id='{}')".format(self.user_id)

    def to_dict(self):
        return {"user_id": self.user_id, "username": self.username}


users = [User(1, "user1", "abcxyz",("group1","group2")), User(2, "user2", "abcxyz",("group1","group3"))]

username_table = {u.username: u for u in users}
userid_table = {u.user_id: u for u in users}


async def authenticate(request, *args, **kwargs):


    # perform the kerberos/spnego handshake
    # reject with 401 if there is authorization header

    if "Authorization" not in request.headers:
        #send 401
        err = Unauthorized("Auth required hallo.", headers={'www-authenticate': 'negotiate'})
        raise err
        #return empty(status=401, headers={'www-authenticate': 'negotiate'}) # this gives a 500
    
    username = request.json.get("username", None)
    password = request.json.get("password", None)

    if not username or not password:
        raise exceptions.AuthenticationFailed("Missing username or password.")

    user = username_table.get(username, None)
    if user is None:
        raise exceptions.AuthenticationFailed("User not found.")

    if password != user.password:
        raise exceptions.AuthenticationFailed("Password is incorrect.")

    return user

i’ve read through all the various rfcs etc and i cannot see anything that i might have missed…

not sure where the code is for the sanic web server engine to be able to instrument that and see why the header is never put on the wire…

This looks like a bug. I am going to get a patch in place for this and will probably release it soon.