diff options
-rw-r--r-- | .gitignore | 9 | ||||
-rw-r--r-- | Cargo.toml (renamed from rust/Cargo.toml) | 0 | ||||
-rw-r--r-- | README.md | 74 | ||||
-rw-r--r-- | build.rs (renamed from rust/build.rs) | 0 | ||||
-rw-r--r-- | clib/sendxmpp.c (renamed from rust/clib/sendxmpp.c) | 0 | ||||
-rwxr-xr-x | demo.sh | 22 | ||||
-rw-r--r-- | functions.py | 80 | ||||
-rwxr-xr-x | login-freebsd.sh | 3 | ||||
-rwxr-xr-x | login.py | 38 | ||||
-rwxr-xr-x | login_test.py | 20 | ||||
-rw-r--r-- | rust/.gitignore | 4 | ||||
-rw-r--r-- | rust/README.md | 40 | ||||
-rwxr-xr-x | server.py | 82 | ||||
-rw-r--r-- | src/apachelog.rs (renamed from rust/src/apachelog.rs) | 0 | ||||
-rw-r--r-- | src/handler.rs (renamed from rust/src/handler.rs) | 0 | ||||
-rw-r--r-- | src/main.rs (renamed from rust/src/main.rs) | 0 | ||||
-rw-r--r-- | src/message.rs (renamed from rust/src/message.rs) | 0 | ||||
-rw-r--r-- | src/sendxmpp.rs (renamed from rust/src/sendxmpp.rs) | 0 | ||||
-rw-r--r-- | src/token.rs (renamed from rust/src/token.rs) | 0 |
19 files changed, 33 insertions, 339 deletions
@@ -1,3 +1,6 @@ -__pycache__ -*swp -*~ +/target/ +/.idea/ +/rust.iml + +# don't care about it now +/Cargo.lock diff --git a/rust/Cargo.toml b/Cargo.toml index 5b183c4..5b183c4 100644 --- a/rust/Cargo.toml +++ b/Cargo.toml @@ -1,60 +1,40 @@ -# apache-auth-xmppmessage +# auth-xmppessage -Authenticate users using tokens sent via xmpp. +### Compile -This script is almost stateless, there is no database required. -To protect against DoS it uses a lockfile, this way allowing only on -instance at a time. +It's written in rust, compile it with `cargo build`. -## Install requirements +### Run - # pip - pip3 install --user -r sleekxmpp==1.3.1 +``` +Usage: ./target/debug/auth_xmppmessage [options] - # FreeBSD: - pkg install py34-sleekxmpp - pkg install ap24-mod_authnz_external24 +Options: + -j, --jid JID bot jid + -p, --password PASSWORD + bot password + -s, --secret SECRET server secret for token generation + -t, --time HOURS Validity of the token in hours (default 48) + -o, --port PORT TCP Port to listen on + -d, --debug Use loglevel Debug instead of Warn + -n, --nosend Don't send XMPP message, just print debug infos + -h, --help print this help menu +``` +### Nginx configuration -## Configuration - - DefineExternalAuth xmpp-login pipe /usr/local/etc/apache24/login.py - <Location /foo> - AuthType Basic - AuthName "Login with Jabber ID and empty password to request a token" - AuthBasicProvider external - AuthExternalContext "validsec=7200;secret=adsasd;users=user1@jabber.org,user2@jabber.org;jid=bot@jabber.org;jid_pw=secret-xmpp-pw" - AuthExternal xmpp-login - Require valid-user - </Location> - -### Options - -- validsec: timespan in which a token is valid. - There are always 2 valid tokens, the current and the previous. - The current is `token(now % validsec)`. The previous is `token(now % validsec - validsec)`. - A token valid-range is determined by `% validsec` and NOT by the time the token was requested. -- secret: random secret data. Used as a salt for the token. -- users: comma separated list of JIDs that are allowed to receive tokens. - Tokens are user-specific. User `A` cannot use the token from user `B`. -- jid: JID of the bot who sends the tokens to the users. -- jid\_pw: password of the bot. - - - -# nginx - -## configuration - - location /grafana { - auth_request /_auth; - # ... - } - +``` location = /_auth { - proxy_pass http://localhost:8081/; + proxy_pass http://127.0.0.1:8081/; # --port PORT proxy_pass_request_body off; proxy_set_header Content-Length ""; proxy_set_header X-Original-URI "$scheme://$host$request_uri"; + proxy_set_header X-Allowed-Jid "JID1,JID2"; } + location /app { + satisfy any; + auth_request /_auth; + deny all; + } +``` diff --git a/rust/clib/sendxmpp.c b/clib/sendxmpp.c index 3f06202..3f06202 100644 --- a/rust/clib/sendxmpp.c +++ b/clib/sendxmpp.c diff --git a/demo.sh b/demo.sh deleted file mode 100755 index 67cb4dc..0000000 --- a/demo.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/sh -# Usage: -# - No arguments: Request a token -# - With arguments: Verify a token -export IP=1.2.3.4 -export URI=/test -export HTTP_HOST=www.example.com; -export CONTEXT="validsec=60;secret=asdsad;users=yvesf@xapek.org,marc@xapek.org;jid=___;jid_pw=___" -export SKIP_XMPP=1 - -if [ -z "$1" ]; then # request token - ( - echo "yvesf@xapek.org" - echo "" - ) | ./login.py -else # verify token - ( - echo "yvesf@xapek.org" - echo "$1" - ) | ./login.py - echo "Result $?" -fi diff --git a/functions.py b/functions.py deleted file mode 100644 index e26e43f..0000000 --- a/functions.py +++ /dev/null @@ -1,80 +0,0 @@ -import os -import re -import time -import struct -import hashlib -from urllib.parse import quote as urlencode - - -def _normalize_token(token): - return re.sub(r"[^A-F0-9]", "", token.upper()) - - -def _generate_token(username, secret, time): - input = "{}{}{}".format(secret, username, time).encode('utf-8') - output = struct.unpack(b"<L", hashlib.md5(input).digest()[:4])[0] - token = "{:02X}-{:02X}-{:02X}".format( - (output >> 16) & 0xff, (output >> 8) & 0xff, output & 0xff) - return token - - -def file_lock(lock_file): - from contextlib import contextmanager - - @contextmanager - def file_lock(): - try: - with open(lock_file, "x") as fh: - try: - yield - except: - raise - finally: - fh.close() - os.remove(lock_file) - except FileExistsError: - raise Exception("Locking failed on {}".format(lock_file)) - - return file_lock() - - -def token_message(username, secret, validsec, url): - time_now = int(time.time()) - time_now_start = int(time_now - time_now % validsec) - time_next_end = time_now_start + 2 * validsec - token = _generate_token(username, secret, time_now_start) - message = "Username: {} Token: {}".format(username, token) - message += "\nValid from: {} to: {}".format( - time.strftime("%c %Z(%z)", time.gmtime(time_now_start)), - time.strftime("%c %Z(%z)", time.gmtime(time_next_end))) - if url is not None: - message += re.sub('(https?://)(.*)', - ' \\1' + urlencode(username) + ':' + urlencode(token) + '@\\2', - url) - return message - - -def send_message(jid, password, recipient, message): - import sleekxmpp - - def start(event): - cl.send_message(mto=recipient, mtype='chat', mbody=message) - cl.disconnect(wait=True) - - cl = sleekxmpp.ClientXMPP(jid, password) - cl.add_event_handler("session_start", start, threaded=True) - if cl.connect(): - cl.process(block=True) - else: - raise Exception("Unable to connect to xmpp server") - - -def verify_token(username, password, conf_secret, conf_validsec): - time_now = int(time.time()) - time_now_start = int(time_now - time_now % conf_validsec) - time_prev_start = time_now_start - conf_validsec - valid_tokens = list(map(_normalize_token, ( - _generate_token(username, conf_secret, time_now_start), - _generate_token(username, conf_secret, time_prev_start) - ))) - return _normalize_token(password) in valid_tokens diff --git a/login-freebsd.sh b/login-freebsd.sh deleted file mode 100755 index f943eec..0000000 --- a/login-freebsd.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -dir="`dirname \"$0\"`" -/usr/local/bin/python3.4 "$dir/"login.py $* diff --git a/login.py b/login.py deleted file mode 100755 index 24f62a1..0000000 --- a/login.py +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env python3.4 -import os -import sys -import functions - - -def run(config): - conf_users = config['users'].split(',') - conf_secret = config['secret'] - conf_validsec = int(config['validsec']) - conf_jid = config['jid'] - conf_jid_pw = config['jid_pw'] - - # reading the credential supplied in a pipe from apache - username = sys.stdin.readline().strip() - password = sys.stdin.readline().strip() - - if password == "" and username in conf_users: - # avoid spamming by allowing only one message sent at a time - lockfile = os.path.basename(__file__) - with functions.file_lock("/tmp/lock." + lockfile): - message = functions.token_message(username, conf_secret, conf_validsec, - os.getenv("URI"), os.getenv("HTTP_HOST")) - if os.getenv("SKIP_XMPP"): # used for testing - print(message) - else: - functions.send_message(conf_jid, conf_jid_pw, username, message) - elif username in conf_users: - if functions.verify_token(username, password, conf_secret, conf_validsec): - return os.EX_OK - - return os.EX_NOPERM # fail by default - - -if __name__ == "__main__": - config = dict(map(lambda kv: kv.split("="), - os.getenv("CONTEXT").split(";"))) - sys.exit(run(config)) diff --git a/login_test.py b/login_test.py deleted file mode 100755 index 2bdbef3..0000000 --- a/login_test.py +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env python3 -import unittest -import functions - - -class TestStringMethods(unittest.TestCase): - def test_normalize(self): - self.assertEqual(functions._normalize_token("A4-B4-C5"), - "A4B4C5") - self.assertEqual(functions._normalize_token("a4-b4-c5"), - "A4B4C5") - self.assertEqual(functions._normalize_token("a4b4c5"), - "A4B4C5") - self.assertEqual(functions._normalize_token("A4B4C5"), - "A4B4C5") - - -if __name__ == '__main__': - unittest.main() - diff --git a/rust/.gitignore b/rust/.gitignore deleted file mode 100644 index 3876e6f..0000000 --- a/rust/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -target/ - -# don't care about it now -Cargo.lock diff --git a/rust/README.md b/rust/README.md deleted file mode 100644 index 7c07af9..0000000 --- a/rust/README.md +++ /dev/null @@ -1,40 +0,0 @@ -# auth-xmppessage - -### Compile - -It's written in rust, compile it with `cargo build` - -### Run - -``` -Usage: ./target/debug/auth_xmppmessage [options] - -Options: - -j, --jid JID bot jid - -p, --password PASSWORD - bot password - -s, --secret SECRET server secret for token generation - -t, --time HOURS Validity of the token in hours (default 48) - -o, --port PORT TCP Port to listen on - -d, --debug Use loglevel Debug instead of Warn - -n, --nosend Don't send XMPP message, just print debug infos - -h, --help print this help menu -``` - -### Nginx configuration - -``` - location = /_auth { - proxy_pass http://127.0.0.1:8081/; # --port PORT - proxy_pass_request_body off; - proxy_set_header Content-Length ""; - proxy_set_header X-Original-URI "$scheme://$host$request_uri"; - proxy_set_header X-Allowed-Jid "JID1,JID2"; - } - - location /app { - satisfy any; - auth_request /_auth; - deny all; - } -```
\ No newline at end of file diff --git a/server.py b/server.py deleted file mode 100755 index 32f022a..0000000 --- a/server.py +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env python3 -import time -import binascii -import random -import argparse -import functions -import logging -from http.server import BaseHTTPRequestHandler, HTTPServer -from socketserver import ThreadingMixIn - -logging.basicConfig(level=logging.INFO) - -LAST_REQUEST_TIME = 0 -CACHE = {} - - -def send_token(conf, username, orig_uri): - message = functions.token_message(username, conf.secret, conf.validsec, orig_uri) - if conf.skip_xmpp: # used for testing - print(message) - else: - functions.send_message(conf.jid, conf.password, username, message) - - -class ThreadingSimpleServer(ThreadingMixIn, HTTPServer): - pass - - -class RequestHandler(BaseHTTPRequestHandler): - def do_GET(self): - global LAST_REQUEST_TIME, CACHE - if 'Authorization' in self.headers: - method, value = self.headers['Authorization'].split(' ') - if method != 'Basic': - self.send_response(400, 'Unsupported authentication method') - elif value in CACHE and CACHE[value] > time.time() - 60: # cache cred for 60s for performance - logging.info("Authorized (cached) %s", value) - self.send_response(200, "OK go forward") - else: - username, password = binascii.a2b_base64(value.encode('utf-8')).decode('utf-8').split(':') - if password == "" and username in conf.users: - if LAST_REQUEST_TIME == 0 or time.time() - LAST_REQUEST_TIME > 15: # max 1 msg per 15 sec - LAST_REQUEST_TIME = time.time() - send_token(conf, username, self.headers['X-Original-URI']) - self.send_response(401, "Token sent, retry") - self.send_header("WWW-Authenticate", "Basic realm=\"xmppmessage auth\"") - else: - self.send_response(429, 'Too Many Requests') - else: - if functions.verify_token(username, password, conf.secret, conf.validsec): - logging.info("Authorized %s", username) - CACHE[value] = time.time() - self.send_response(200, "OK go forward") - else: - logging.info("Denied %s", username) - self.send_response(403, "Authentication failed, username or password wrong") - else: - self.send_response(401) - self.send_header("WWW-Authenticate", "Basic realm=\"xmppmessage auth\"") - - self.end_headers() - - -def run(conf): - httpd = HTTPServer((conf.server_host, conf.server_port), RequestHandler) - httpd.conf = conf - httpd.serve_forever() - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument('--secret', default="".join([chr(random.randint(ord('0'), ord('Z'))) for x in range(20)])) - parser.add_argument('--validsec', type=int, default=60 * 60 * 48) - parser.add_argument('--user', '-u', nargs='+', default=['yvesf@xapek.org', 'marc@xapek.org'], dest='users') - parser.add_argument('--jid', help="Bot jid", default="bot@xapek.org") - parser.add_argument('--password', help="Bot jid password") - parser.add_argument('--server-host', default="127.0.0.1") - parser.add_argument('--server-port', default=8081, type=int) - parser.add_argument('--skip-xmpp', default=False, type=bool) - - conf = parser.parse_args() - run(conf) diff --git a/rust/src/apachelog.rs b/src/apachelog.rs index fb5d1a6..fb5d1a6 100644 --- a/rust/src/apachelog.rs +++ b/src/apachelog.rs diff --git a/rust/src/handler.rs b/src/handler.rs index 97f5d5c..97f5d5c 100644 --- a/rust/src/handler.rs +++ b/src/handler.rs diff --git a/rust/src/main.rs b/src/main.rs index 30d4f8b..30d4f8b 100644 --- a/rust/src/main.rs +++ b/src/main.rs diff --git a/rust/src/message.rs b/src/message.rs index 0af1a13..0af1a13 100644 --- a/rust/src/message.rs +++ b/src/message.rs diff --git a/rust/src/sendxmpp.rs b/src/sendxmpp.rs index 2db471e..2db471e 100644 --- a/rust/src/sendxmpp.rs +++ b/src/sendxmpp.rs diff --git a/rust/src/token.rs b/src/token.rs index 2a2e446..2a2e446 100644 --- a/rust/src/token.rs +++ b/src/token.rs |