Search code examples
pythontwistedtwistd

How to disable HTTP Basic Auth for specific API endpoints on Twisted-Klein server


I have simple Twisted-Klein server, with HTTP Basic Auth enabled globally:

from klein import Klein
import attr
from zope.interface import implementer
from twisted.cred.portal import IRealm
from twisted.internet.defer import succeed
from twisted.cred.portal import Portal
from twisted.cred.checkers import FilePasswordDB
from twisted.web.resource import IResource
from twisted.web.guard import HTTPAuthSessionWrapper, BasicCredentialFactory
from werkzeug.datastructures import MultiDict
from bson import json_util
import json


app = Klein()


# health check
@app.route('/health', methods=['GET'])
def health_check(request):
    return ''


# dataset query API
@app.route('/query/<path:expression>', methods=['GET'])
def query(request, expression):
    response = evaluate_expression(expression)
    return response


@implementer(IRealm)
@attr.s
class HTTPAuthRealm(object):
    resource = attr.ib()

    def requestAvatar(self, avatarId, mind, *interfaces):
        return succeed((IResource, self.resource, lambda: None))


def resource():
    realm = HTTPAuthRealm(resource=app.resource())
    portal = Portal(realm, [FilePasswordDB('./configs/server-auth.db')])
    credential_factory = BasicCredentialFactory('Authentication required')
    return HTTPAuthSessionWrapper(portal, [credential_factory])

I want to disable auth for only specific API endpoints, for example, in this case for /health API endpoint. I've read the docs, but just cant wrap my mind around it.


Solution

  • One way is to only wrap the part of the hierarchy that you want authentication for:

    from twisted.web.resource import Resource
    
    class Health(Resource):
        # ...
    
    def resource():
        realm = HTTPAuthRealm(resource=app.resource())
        portal = Portal(realm, [FilePasswordDB('./configs/server-auth.db')])
        credential_factory = BasicCredentialFactory('Authentication required')
        guarded = HTTPAuthSessionWrapper(portal, [credential_factory])
    
        root = Resource()
        root.putChild(b"health", Health())
        root.putChild(b"this-stuff-requires-auth", guarded)
    
        return root
    

    The normal resource traversal logic used for dispatching requests will start at root. If the request is for /health (or any child) then it goes to root's health child - which is the Health instance created in this example. Note how the HTTPAuthSessionWrapper doesn't get involved there. If the request is for /this-stuff-requires-auth (or any child) then traversal does go through the auth wrapper and so authentication is required.

    Another approach is to vary your avatar based on the credentials. In this scheme, you actually still authenticate everyone but you authorize anonymous users to access some of the hierarchy.

    from twisted.cred.checkers import ANONYMOUS
    
    @implementer(IRealm)
    @attr.s
    class HTTPAuthRealm(object):
        def requestAvatar(self, avatarId, mind, *interfaces):
            avatar = Resource()
            avatar.putChild(b"health", Health())
            if avatarId is not ANONYMOUS:
                avatar.putChild(b"this-stuff-requires-auth", SecretResource())
            return succeed((IResource, avatar, lambda: None))
    

    You'll also need to configure your portal with a credentials checker for anonymous credentials:

    from twisted.cred.checkers import AllowAnonymousAccess
    
    portal = Portal(
        realm, [
            FilePasswordDB('./configs/server-auth.db'),
            AllowAnonymousAccess(),
        ],
    )
    

    In this approach, HTTPAuthSessionWrapper is again your root resource.

    Anonymous requests are associated with the ANONYMOUS avatar identifier and HTTPAuthRealm gives back an IResource which only knows about the resources that should be available to anonymous users.

    Requests with valid user credentials are associated with a different avatar identifier (usually their username) and HTTPAuthRealm gives back an IResource with more children attached to it, granting more access.