From ca534003b3f07d2045dc1cbb6cddf60223fd3385 Mon Sep 17 00:00:00 2001 From: Holden Rohrer Date: Fri, 12 May 2023 21:47:54 -0400 Subject: initial commit --- .gitignore | 4 ++++ LICENSE | 13 +++++++++++ Makefile | 23 +++++++++++++++++++ README | 41 +++++++++++++++++++++++++++++++++ nginx.example.conf | 52 ++++++++++++++++++++++++++++++++++++++++++ src/login.html | 55 ++++++++++++++++++++++++++++++++++++++++++++ src/login.py | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ test.pem | 3 +++ weblogin.ini | 8 +++++++ weblogin.spec.in | 54 +++++++++++++++++++++++++++++++++++++++++++ webpass | 1 + 11 files changed, 321 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README create mode 100644 nginx.example.conf create mode 100644 src/login.html create mode 100644 src/login.py create mode 100644 test.pem create mode 100644 weblogin.ini create mode 100644 weblogin.spec.in create mode 100644 webpass diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5964d2d --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +weblogin.tar.gz +*.rpm +dist +weblogin.spec diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f88cad3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright 2023 Holden Rohrer + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the “Software”), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..40d7d9f --- /dev/null +++ b/Makefile @@ -0,0 +1,23 @@ +NAME = weblogin +SRC = src/login.html src/login.py LICENSE README +VER = 1.0 +REL = 0 + +dist: $(NAME).tar.gz $(NAME).spec + mkdir -pv ~/rpmbuild/SOURCES + cp $(NAME).tar.gz ~/rpmbuild/SOURCES + rpmbuild $(NAME).spec -ba + cp ~/rpmbuild/RPMS/noarch/$(NAME)-$(VER)-$(REL).noarch.rpm . + touch dist + +clean: + rm -f $(NAME).tar.gz $(NAME)-$(VER)-$(REL).noarch.rpm dist $(NAME).spec + +$(NAME).tar.gz: $(SRC) + mkdir $(NAME)-$(VER) + cp -r -t $(NAME)-$(VER) $(SRC) + tar -czf $(NAME).tar.gz $(NAME)-$(VER) + rm -rf $(NAME)-$(VER) + +$(NAME).spec: $(NAME).spec.in + sed -e 's/VERSION/$(VER)/' -e 's/RELEASE/$(REL)/' $< > $@ diff --git a/README b/README new file mode 100644 index 0000000..67be190 --- /dev/null +++ b/README @@ -0,0 +1,41 @@ +# weblogin + +This is a small flask webserver that returns a JSON web token for form +requests at /auth, submitted by a user of the /login.html page. +A GET request to /logout will remove the JSON web token cookie (the +cookie auth is used for storage). + +## Requirements + +- Python 3 +- flask +- passlib +- python-jwt + +## License + +Licensed under the MIT license. +It just isn't a big enough piece of software to justify anything +more restrictive. + +## Hello World + +To try the code out yourself, +install the `nginx.example.conf` as /etc/nginx/nginx.conf and +install the `src/login.html` + +```sh +systemctl enable --now nginx +python src/login.py webpass test.pem +``` +Now connect to localhost, and the username is hr and the password is +soupsoup. + +## Other stuff around here + +`weblogin.ini` is an ini file for compatibility with uwsgi that should +help making that setup easier if you plan to use uwsgi. +And the Makefile and the specfile are for building an RPM for Fedora +(may also work on CentOS/RHEL). +To use those, you need some make program and rpmbuild installed. +I might publish the RPM at some point. diff --git a/nginx.example.conf b/nginx.example.conf new file mode 100644 index 0000000..757b614 --- /dev/null +++ b/nginx.example.conf @@ -0,0 +1,52 @@ +load_module modules/ngx_http_jwted_module.so; + +#user html; +worker_processes 1; + +error_log /var/log/nginx/error.log; + +#pid logs/nginx.pid; + + +events { + worker_connections 1024; +} + +http { + include mime.types; + + auth_jwt_key 'WKaDRAk7hx4ZiY5zV3H6cnGCzloBgWbV2FC1poLfzLY='; + auth_jwt_cache on; + + server { + listen 80; + server_name localhost; + #charset koi8-r; + + location = /auth { + proxy_pass http://localhost:5000; + } + + location = /logout { + proxy_pass http://localhost:5000; + } + + #access_log logs/host.access.log main; + location = /login { + root /var/lib/weblogin; + try_files $uri.html =404; + } + + location @login_redirect { + return 302 /login; + } + + location / { + auth_jwt $cookie_auth; + error_page 401 = @login_redirect; + root /usr/share/nginx/html; + } + + } + +} diff --git a/src/login.html b/src/login.html new file mode 100644 index 0000000..cb35622 --- /dev/null +++ b/src/login.html @@ -0,0 +1,55 @@ + + + + + + + + +
+

Login Page

+
+
+
+
+ +
Could not verify login
+
+
+ + diff --git a/src/login.py b/src/login.py new file mode 100644 index 0000000..2bfe825 --- /dev/null +++ b/src/login.py @@ -0,0 +1,67 @@ +#!/usr/bin/python3 +from passlib.apache import HtpasswdFile +from flask import Flask, request, make_response, jsonify, redirect +import python_jwt as jwt +from jwcrypto.jwk import JWK +import datetime +from json import dumps +import argparse + +app = Flask(__name__) + +def authorized_request(): + args = request.form + + user = args['user'] if 'user' in args else None + pswd = args['pass'] if 'pass' in args else None + remember = args['remember'] if 'remember' in args else None + + valid = user is not None and pswd is not None + valid = valid and user in htpasswd.users() + valid = valid and htpasswd.check_password(user, pswd) + + return valid, user, pswd, remember + +@app.route('/auth', methods=['POST']) +def authorize(): + # on success, redirect to /. on failure, redirect to /login + auth, user, _, remember = authorized_request() + if auth: + resp = redirect('/') + if remember: + exp = None + else: + exp = datetime.timedelta(minutes=exptime) + token = jwt.generate_jwt({}, privkey, "EdDSA", exp) + resp.set_cookie('auth', token, max_age=exp) + return resp + # this stuff too + else: + resp = redirect('/login?err') + return resp + +@app.route('/logout', methods=['GET']) +def logout(): + resp = redirect('/login') + resp.delete_cookie('auth') + return resp + +if __name__ == '__main__': + # argparse arguments + parser = argparse.ArgumentParser( + prog='login.py', + description='A web server that handles htpasswd-file JWT auth logic') + parser.add_argument('htpasswd') + parser.add_argument('privkey') + parser.add_argument('-e', '--expireminutes', default=30, type=int) + + args = parser.parse_args() + htpasswd_filename = args.htpasswd + privkey_filename = args.privkey + exptime = args.expireminutes + + htpasswd = HtpasswdFile(htpasswd_filename) + with open(privkey_filename, 'rb') as privkey_file: + privkey = JWK() + privkey.import_from_pem(privkey_file.read()) + app.run(debug=True) diff --git a/test.pem b/test.pem new file mode 100644 index 0000000..e06d885 --- /dev/null +++ b/test.pem @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEII3XeGsOaqd1oCs25fWJrUh27zT++OJlakO0Xq/7jaNq +-----END PRIVATE KEY----- diff --git a/weblogin.ini b/weblogin.ini new file mode 100644 index 0000000..c368321 --- /dev/null +++ b/weblogin.ini @@ -0,0 +1,8 @@ +[uwsgi] +master = true +socket = /run/uwsgi/%n.sock +wsgi-file = /usr/lib/weblogin/login.py +pyargv=/etc/webloginpasswd /var/lib/weblogin/key.pem +procname-master = uwsgi weblogin +uid = weblogin +gid = weblogin diff --git a/weblogin.spec.in b/weblogin.spec.in new file mode 100644 index 0000000..ac63130 --- /dev/null +++ b/weblogin.spec.in @@ -0,0 +1,54 @@ +Summary: A login page for websites that provides a JSON Web Token +Name: weblogin +Version: VERSION +Release: RELEASE +License: MIT +URL: https://git.hrhr.dev/weblogin/about +Distribution: Fedora 38 +Vendor: Holden Rohrer +Packager: Holden Rohrer +BuildArch: noarch +Requires: python3 +Requires: python3-flask +Requires: python3-passlib +Requires: python3-jwt+crypto + +Source: weblogin.tar.gz + +%define username weblogin +%define installdir /usr/lib/%{username} +%define sharedir /usr/share/%{username} +%define vardir /var/lib/%{username} + +%description +This is a small flask webserver that returns a JSON web token for form +requests at /auth, submitted by a user of the /login.html page. +A GET request to /logout will remove the JSON web token cookie (the +cookie auth is used for storage). + +%global debug_package %{nil} + +%pre +getent group %{username} >/dev/null || groupadd -r %{username} +getent passwd %{username} >/dev/null || useradd -r -s /sbin/nologin\ + -g %{username} -c "weblogin server" -d %{installdir} -M %{username} + +%prep +%autosetup + +%install +mkdir -p %{buildroot}%{installdir} %{buildroot}%{sharedir} %{buildroot}%{vardir} +ls +cp -t %{buildroot}%{installdir} login.py +cp -t %{buildroot}%{sharedir} login.html + +%clean +rm -rf %{buildroot} + +%files +%doc README +%license LICENSE +%defattr(644, %{username}, %{username}, 755) +%attr(755, weblogin, weblogin) %{installdir}/login.py +%{sharedir}/login.html +%attr(600, weblogin, weblogin) %{vardir} diff --git a/webpass b/webpass new file mode 100644 index 0000000..9a015aa --- /dev/null +++ b/webpass @@ -0,0 +1 @@ +hr:$2y$05$mAxt1IZh0gRphc3UX2n3G.f97AWVR9tAMccrW8pYiEhRwHM8btvdm -- cgit