Compare commits

..

No commits in common. "2c873ba74d2ed9dc5ba2e024bfa1518e512aebf6" and "3d755031e49ed6d48a10c2098534affb264a9bd6" have entirely different histories.

3 changed files with 14 additions and 49 deletions

View file

@ -89,9 +89,6 @@ the client does not support Kerberos. To use this, configure:
There should be one `%s` symbol in this string, which will be replaced by the
username.
You may also choose to exclusively use LDAP without using any Kerberos or GSSAPI
by setting the environment variable `KRBAUTH_DISABLE_GSSAPI=yes`.
### TLS Client Certificate
It's also possible to use client certificates on machines that have them for
@ -113,16 +110,6 @@ ssl_client_certificate /path/to/ca.crt;
ssl_verify_client optional;
```
### Rate limiting
`nginx-krbauth` supports rate limiting. The rate limiting frequency can be
configured by `KRBAUTH_LIMITER_FREQUENCY` environment variable. The default is
`10 / 5 minute`, but you can adjust this as needed.
The rate limiting state is stored in memory. You can use any
[storage mechanism][limits-storage] supported by the `limits` PyPI package.
Remember to install any dependencies!
## Example `nginx.conf`
```nginx
@ -141,5 +128,3 @@ location /krbauth {
include uwsgi_params;
}
```
[limits-storage]: https://limits.readthedocs.io/en/stable/storage.html#storage-scheme

View file

@ -4,7 +4,6 @@ import hashlib
import hmac
import logging
import os
import re
import struct
import time
from typing import Optional
@ -12,10 +11,7 @@ from typing import Optional
import gssapi
import ldap
from flask import Flask, Response, redirect, request
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from gssapi.exceptions import BadMechanismError, GSSError, GeneralError
from ldap.filter import escape_filter_chars
from werkzeug.routing import Rule
app = Flask(__name__)
@ -23,10 +19,6 @@ app.logger.setLevel(logging.INFO)
app.url_map.add(Rule('/krbauth', endpoint='krbauth.auth'))
app.url_map.add(Rule('/krbauth/check', endpoint='krbauth.check'))
LIMITER_STORAGE = os.environ.get('KRBAUTH_LIMITER_STORAGE', 'memory://')
LIMITER_FREQUENCY = os.environ.get('KRBAUTH_LIMITER_FREQUENCY', '10 / 5 minute')
limiter = Limiter(get_remote_address, app=app, storage_uri=LIMITER_STORAGE)
timestamp = struct.Struct('!q')
hmac_digest = hashlib.sha512
digest_size = hmac_digest().digest_size
@ -41,7 +33,6 @@ LDAP_SEARCH_BASE = os.environ.get('KRBAUTH_LDAP_SEARCH_BASE')
LDAP_USER_DN = os.environ.get('KRBAUTH_LDAP_USER_DN')
assert not LDAP_USER_DN or LDAP_USER_DN.count('%s') == 1
ENABLE_GSSAPI = os.environ.get('KRBAUTH_DISABLE_GSSAPI', '0').lower() not in ('1', 'yes')
GSSAPI_NAME = os.environ.get('KRBAUTH_GSSAPI_NAME')
if GSSAPI_NAME:
gssapi_name = gssapi.Name(GSSAPI_NAME, gssapi.NameType.hostbased_service)
@ -104,7 +95,7 @@ def make_401(reason: str, negotiate: Optional[str] = 'Negotiate', **kwargs) -> R
</body>
</html>
''' % (reason,), status=401)
if ENABLE_GSSAPI and negotiate:
if negotiate:
resp.headers.add('WWW-Authenticate', negotiate)
if LDAP_USER_DN:
resp.headers.add('WWW-Authenticate', 'Basic')
@ -141,13 +132,10 @@ def auth_spnego(context: Context, next_url: str) -> Response:
ldap_ctx = ldap.initialize(LDAP_SERVER)
if LDAP_BIND_DN and LDAP_BIND_AUTHTOK:
ldap_ctx.bind_s(LDAP_BIND_DN, LDAP_BIND_AUTHTOK, ldap.AUTH_SIMPLE)
ldap_filter = '(&(memberOf=%s)(krbPrincipalName=%s))' % (
escape_filter_chars(context.ldap_group),
escape_filter_chars(str(krb5_name)),
)
ldap_filter = '(&(memberOf=%s)(krbPrincipalName=%s))' % (context.ldap_group, krb5_name)
result = ldap_ctx.search_s(LDAP_SEARCH_BASE, ldap.SCOPE_SUBTREE, ldap_filter, ['cn'])
if not result:
return make_401('Authentication failed', krb5_name=krb5_name)
return make_401('Did not find LDAP group member', krb5_name=krb5_name)
app.logger.info('Authenticated via Kerberos as: %s, %s', krb5_name, result[0][0])
else:
app.logger.info('Authenticated via Kerberos as: %s', krb5_name)
@ -155,10 +143,6 @@ def auth_spnego(context: Context, next_url: str) -> Response:
return auth_success(context, next_url)
def is_sane_username(username: str) -> bool:
return len(username) <= 64 and re.match(r'^[a-zA-Z0-9._@-]+$', username) is not None
def auth_basic(context: Context, next_url: str) -> Response:
try:
token = base64.b64decode(request.headers['Authorization'][6:])
@ -166,8 +150,8 @@ def auth_basic(context: Context, next_url: str) -> Response:
except (binascii.Error, UnicodeDecodeError):
return Response(status=400)
if not username or not is_sane_username(username) or not password:
return make_401('Authentication failed')
if not username or not password:
return make_401('Invalid username or password')
assert LDAP_USER_DN is not None
dn = LDAP_USER_DN % (username,)
@ -175,16 +159,14 @@ def auth_basic(context: Context, next_url: str) -> Response:
try:
ldap_ctx.bind_s(dn, password)
except ldap.INVALID_CREDENTIALS:
return make_401('Authentication failed', dn=dn)
return make_401('Failed to authenticate to LDAP', dn=dn)
if context.ldap_group:
if not ldap_ctx.search_s(dn, ldap.SCOPE_BASE, '(memberof=%s)' % (
escape_filter_chars(context.ldap_group),
)):
return make_401('Authentication failed', dn=dn, group=context.ldap_group)
app.logger.info('Authenticated via LDAP as: %s in %s from %s', dn, context.ldap_group, request.remote_addr)
if not ldap_ctx.search_s(dn, ldap.SCOPE_BASE, '(memberof=%s)' % (context.ldap_group,)):
return make_401('Did not find LDAP group member', dn=dn, group=context.ldap_group)
app.logger.info('Authenticated via LDAP as: %s in %s', dn, context.ldap_group)
else:
app.logger.info('Authenticated via LDAP as: %s from %s', dn, request.remote_addr)
app.logger.info('Authenticated via LDAP as: %s', dn)
return auth_success(context, next_url)
@ -197,16 +179,14 @@ def check_tls() -> bool:
@app.endpoint('krbauth.auth')
@limiter.limit(LIMITER_FREQUENCY)
def auth() -> Response:
next_url = request.args.get('next', '/')
context = Context.from_request()
authorization = request.headers.get('Authorization', '')
if check_tls():
# No cookie required since the check endpoint can trivially verify mTLS.
return redirect(next_url, code=307)
if ENABLE_GSSAPI and authorization.startswith('Negotiate '):
return auth_success(context, next_url)
if authorization.startswith('Negotiate '):
return auth_spnego(context, next_url)
if LDAP_USER_DN and authorization.startswith('Basic '):
return auth_basic(context, next_url)

View file

@ -7,9 +7,9 @@ with open(os.path.join(os.path.dirname(__file__), 'README.md')) as f:
setup(
name='nginx_krbauth',
version='0.0.5',
version='0.0.4',
py_modules=['nginx_krbauth'],
install_requires=['flask', 'gssapi', 'python-ldap', 'flask-limiter'],
install_requires=['flask', 'gssapi', 'python-ldap'],
author='quantum',
author_email='quantum2048@gmail.com',