Skip to content

ASGI Kerberos/GSSAPI Authentication Middleware with delegation support

License

Notifications You must be signed in to change notification settings

washed-out/asgi-gssapi

Repository files navigation

ASGI-GSSAPI

ASGI-GSSAPI is ASGI Middleware which implements Kerberos authentication. It makes it easy to add Kerberos authentication to any ASGI application.

Its only dependency is python-gssapi and it's been tested up to version 1.7.2

Unfortunately, as is the case with most things kerberos, it requires a kerberos environment as well as a keytab. Setting that up is outside the scope of this document.

The official copy of this documentation is available at Read the Docs.

Installation

Install the extension with pip:

$ pip install ASGI-GSSAPI

How to Use

To integrate ASGI-GSSAPI into your application you'll need to generate your keytab and set the environment variable KRB5_KTNAME in your shell to the location of the keytab file.

After that, it should be as easy as passing your application to the SPNEGOAuthMiddleware constructor. All requests destined for the application will first be authenticated by the middleware, and the authenticated users principal will be available as the principal key in the ASGI scope dictionary, under gssapi key.

For example:

import uvicorn
from asgi_gssapi import SPNEGOAuthMiddleware

async def example(scope, receive, send):
    await send({
        'type': 'http.response.start',
        'status': 200,
        'headers': [
            [b'content-type', b'text/plain'],
        ],
    })
    await send({
        'type': 'http.response.body',
        'body': b'Hello, {}'.format(scope['gssapi']['principal']),
    })

app = SPNEGOAuthMiddleware(example)

if __name__ == '__main__':
    uvicorn.run(app, port=8080)

ASGI-GSSAPI assumes that every request should be authenticated. If this is not the case, you can override it by passing in a callback named auth_required_callback to the SPNEGOAuthMiddleware constructor. This callback will be called for every request and passed the ASGI scope dictionary:

import uvicorn
from asgi_gssapi import SPNEGOAuthMiddleware

async def example(scope, receive, send):
    ... # same as above

def authenticate(scope):
    return scope['path'].startswith('/protected')

app = SPNEGOAuthMiddleware(example,
                           auth_required_callback=authenticate)

if __name__ == '__main__':
    uvicorn.run(app, port=8080)

By default, when ASGI-GSSAPI responds with a 401 to indicate that authentication is required, it generates a very simple page with a Content-Type of text/plain that includes the string Unauthorized.

Similarly, when it responds with a 403 indicating that authentication has failed, it generates another simple page with a Content-Type of text/plain that includes the string Forbidden.

These can be customized:

import uvicorn
from asgi_gssapi import SPNEGOAuthMiddleware

async def example(scope, receive, send):
    ... # same as above

app = SPNEGOAuthMiddleware(example,
                           unauthorized='Authentication Required',
                           forbidden='Authentication Failed')

if __name__ == '__main__':
    uvicorn.run(app, port=8080)

You can also change the Content-Types by passing in full ASGI event tuples:

import uvicorn
from asgi_gssapi import SPNEGOAuthMiddleware

async def example(scope, receive, send):
    ... # same as above

forbidden=({
    'type': 'http.response.start',
    'status': 403,
    'headers': [
        [b'content-type', b'text/html'],
    ],
}, {
    'type': 'http.response.body',
    'body': b'<html><body><h1>GO AWAY</h1></body></html>'
})

unauthorized=({
    'type': 'http.response.start',
    'status': 401,
    'headers': [
        [b'content-type', b'text/html'],
        [b'www-authenticate', b'negotiate'],
    ],
}, {
    'type': 'http.response.body',
    'body': b'<html><body><h1>LOGIN FIRST</h1></body></html>'
})

app = SPNEGOAuthMiddleware(example,
                           unauthorized=unauthorized,
                           forbidden=forbidden)

if __name__ == '__main__':
    uvicorn.run(app, port=8080)

Hopefully, you are not using raw ASGI, and your framework of choice provides a saner alternatives to full event definitions (like Starlette's Response class).

ASGI-GSSAPI will authenticate the request using auto-resolved hostname. You can change it, by providing the hostname argument to the constructor, or defer to any hostname, present in keytab file, by providing an empty string hostname argument to the constructor:

import uvicorn
from asgi_gssapi import SPNEGOAuthMiddleware

async def example(scope, receive, send):
    ... # same as above

app = SPNEGOAuthMiddleware(example, hostname='example.com')

if __name__ == '__main__':
    uvicorn.run(app, port=8080)

ASGI-GSSAPI provides support for delegation. You do not need to configure anything server-side, and it's up to the client to delegate the credentials. When it does so, you'll receive a gssapi.Credentials object in ASGI scope dictionary, under delegate_creds key:

async def example(scope, receive, send):
    creds = scope['gssapi']['delegate_creds']

Such creds can be exported to a token cache file (using the .export() method), e.g. /tmp/krb5cc_0, or passed directly to any other GSSAPI-powered python code, such as requests-gssapi!

How it works

When an application which uses the middleware is accessed by a client, it will check to see if the request includes authentication credentials in an Authorization header. If there are no such credentials, the application will respond immediately with a 401 Unauthorized response which includes a WWW-Authenticate header field with a value of Negotiate indicating to the client that they are currently unauthorized, but that they can authenticate using Negotiate authentication.

If credentials are presented in the Authorization header, the credentials will be validated, the principal of the authenticating user will be extracted and added to the ASGI scope using the key principal in the gssapi dictionary, and the application will be called to serve the request. Send event will be hijacked to append WWW-Authenticate header which identifies the server to the client. This allows ASGI-GSSAPI to support mutual authentication.

Full Example

To see a simple example, you can download the code from github. It is in the example directory.

Changes

0.1.0 (2021-10-16)

  • initial implementation

API References

The full API reference:

.. automodule:: asgi_gssapi
   :members:

History

Although this plugin shares no code with WSGI-Kerberos , whole repository layout, including the README file you're reading now, was shamelessly stolen from it. Thus, I'm keeping contributors and license intact.