Python CSC Electronic Office
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

69 lines
2.9 KiB

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.
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