pyceo/ceod/api/spnego.py

70 lines
2.9 KiB
Python

from base64 import b64decode, b64encode
import functools
import socket
from typing import Union
from flask import request, Response, make_response, g
import gssapi
from gssapi.raw import RequirementFlag
_server_name = None
def init_spnego(service_name: str, fqdn: Union[str, None] = None):
"""Set the server principal which will be used for SPNEGO."""
global _server_name
if fqdn is None:
fqdn = socket.getfqdn()
_server_name = gssapi.Name('ceod/' + fqdn)
# make sure that we're actually capable of acquiring credentials
gssapi.Credentials(usage='accept', name=_server_name)
def requires_authentication(f):
"""
Requires that all requests to f have a GSSAPI initiator token.
The initiator principal will be passed to the first argument of f
in the form user@REALM.
"""
@functools.wraps(f)
def wrapper(*args, **kwargs):
if 'authorization' not in request.headers:
return Response('Unauthorized', 401, {'WWW-Authenticate': 'Negotiate'})
header = request.headers['authorization']
client_token = b64decode(header.split()[1])
creds = gssapi.Credentials(usage='accept', name=_server_name)
ctx = gssapi.SecurityContext(creds=creds, usage='accept')
server_token = ctx.step(client_token)
# OK so we're going to cheat a bit here by assuming that Kerberos is the
# mechanism being used (which we know will be true). We know that Kerberos
# only requires one round-trip for the service handshake, so we don't need
# to store state between requests. Just to be sure, we assert that this is
# indeed the case.
# (This isn't compliant with the GSSAPI spec, but why write more code than
# necessary?)
assert ctx.complete, 'only one round trip expected'
# Store the username in flask.g
client_princ = str(ctx.initiator_name)
g.auth_user = client_princ[:client_princ.index('@')]
# Store the delegated credentials, if they were given
if ctx.actual_flags & RequirementFlag.delegate_to_peer:
# For some reason, shit gets screwed up when you try to use a
# gssapi.Credentials object which was created in another function.
# So we're going to export the token instead (which is a bytes
# object) and pass it to Credentials() whenever we need it.
g.client_token = ctx.delegated_creds.export()
# TODO: don't pass client_princ to f anymore since it's stored in flask.g
resp = make_response(f(client_princ, *args, **kwargs))
# RFC 2744, section 5.1:
# "If no token need be sent, gss_accept_sec_context will indicate this
# by setting the length field of the output_token argument to zero."
if server_token is not None:
resp.headers['WWW-Authenticate'] = 'Negotiate ' + b64encode(server_token).decode()
return resp
return wrapper