commit 49b228c1a53f5f9b53e35fa83987ae428b39b54d Author: Edward Betts Date: Sun Jan 21 09:10:07 2024 +0000 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..24e7b0e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__ +config diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f6d2506 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Edward Betts + +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/README.md b/README.md new file mode 100644 index 0000000..d81ffb6 --- /dev/null +++ b/README.md @@ -0,0 +1,73 @@ +# UniAuth + +UniAuth is a single sign-on application designed to provide a secure and efficient method for managing user authentication. The application is built using Flask, a popular web framework in Python, and offers a straightforward implementation suitable for integrating into various web services. + +## Features + +- **Token-Based Authentication**: Generates and verifies secure authentication tokens. +- **Login Required Decorator**: Enhances routes to require user authentication. +- **User Authentication**: Validates username and password against stored credentials. +- **Secure Cookie Handling**: Sets authentication tokens in cookies with security best practices. + +## Getting Started + +These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. + +### Prerequisites + +- Python 3 +- Flask +- Werkzeug +- ItsDangerous + +### Installing + +1. Clone the repository: + ``` + git clone https://git.4angle.com/edward/UniAuth + ``` +2. Install the required packages: + ``` + pip install flask werkzeug itsdangerous + ``` +3. Set up the configuration in `config/default.py`. + +### Running the Application + +Run the application using: + +``` +python main.py +``` + +The application should now be running on `http://0.0.0.0:5000/`. + +## Usage + +- **Login**: Users can log in by providing their username and password. +- **Token Generation**: Upon successful login, a secure token is generated and stored in a cookie. +- **Access Protected Routes**: Only authenticated users can access protected routes. + +## Contributing + +Contributions are welcome. If you'd like to contribute to this project, please +follow these steps: + +1. Fork the repository. +2. Create a new branch for your feature or bug fix. +3. Make your changes and commit them with descriptive messages. +4. Push your changes to your fork. +5. Create a pull request to the main repository's `main` branch. + +## License + +This project is licensed under the MIT License. Feel free to use, modify, and distribute it as per the license terms. See the [LICENSE](LICENSE) file for details. + +## Acknowledgments + +- Flask team for the great web framework +- ItsDangerous for secure token generation + +--- + +This README provides a basic overview and guide for your application. You can further customize it to include more detailed documentation as needed. diff --git a/main.py b/main.py new file mode 100755 index 0000000..2eb5400 --- /dev/null +++ b/main.py @@ -0,0 +1,103 @@ +#!/usr/bin/python3 +"""Single sign-on application.""" + +import functools +import typing +from datetime import datetime, timedelta +from typing import Any, Callable + +import flask +import werkzeug +from itsdangerous.url_safe import URLSafeTimedSerializer + +app = flask.Flask(__name__) +app.debug = True +app.config.from_object("config.default") + +serializer = URLSafeTimedSerializer(app.config["SECRET_KEY"]) +max_age = 60 * 60 * 24 * 90 + + +def generate_auth_token(username: str) -> str: + """Generate a secure authentication token.""" + token = typing.cast(str, serializer.dumps(username, salt="auth")) + assert isinstance(token, str) + return token + + +def verify_auth_token(token: str) -> str | None: + """Verify the authentication token.""" + try: + username = serializer.loads(token, salt="auth", max_age=max_age) + except Exception: + return None + + assert isinstance(username, str) + return username + + +def login_required(f: Callable[..., Any]) -> Callable[..., Any]: + """Route requires login decorator.""" + + @functools.wraps(f) + def decorated_function(*args, **kwargs) -> werkzeug.Response: # type: ignore + token = flask.request.cookies.get("auth_token") + if not token or verify_auth_token(token) is None: + # Save the original URL in the session and redirect to login + flask.session["next"] = flask.request.url + return flask.redirect(flask.url_for("login_page")) + return typing.cast(werkzeug.Response, f(*args, **kwargs)) + + return decorated_function + + +@app.route("/") +@login_required +def root_page() -> str: + """Root page.""" + return flask.render_template("auth_good.html") + + +def check_user_auth() -> dict[str, Any] | None: + """Load username and password and check if valid.""" + # Extract username and password from POST request + username = flask.request.form.get("username", type=str) + password = flask.request.form.get("password", type=str) + + # Retrieve users from app config + users = app.config["USERS"] + + # Check if the username and password match any user in the list + return next( + (u for u in users if u["username"] == username and u["password"] == password), + None, + ) + + +@app.route("/login", methods=["GET", "POST"]) +def login_page() -> str | werkzeug.Response: + """Login page.""" + if flask.request.method == "GET": + return flask.render_template("login.html") + + if not (user := check_user_auth()): + # Login failed: Show an error message on the login page + return flask.render_template("login.html", error="Invalid credentials") + + expire_date = datetime.now() + timedelta(days=180) + + response = flask.redirect(flask.session.get("next") or flask.url_for("dashboard")) + response.set_cookie( + "auth_token", + generate_auth_token(user["username"]), + expires=expire_date, + httponly=True, + secure=True, + samesite="Lax", + ) + + return response + + +if __name__ == "__main__": + app.run(host="0.0.0.0") diff --git a/templates/auth_good.html b/templates/auth_good.html new file mode 100644 index 0000000..52a2368 --- /dev/null +++ b/templates/auth_good.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} + +{% block content %} +
+

Auth

+

Login successful.

+
+{% endblock %} diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..7b1cbde --- /dev/null +++ b/templates/base.html @@ -0,0 +1,23 @@ + + + + + + +{% block title %}{% endblock %} + + + + +{% block style %} +{% endblock %} + +{% from "navbar.html" import navbar with context %} + + +{% block nav %}{{ navbar() }}{% endblock %} +{% block content %}{% endblock %} +{% block scripts %}{% endblock %} + + + diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..4eb9fff --- /dev/null +++ b/templates/login.html @@ -0,0 +1,26 @@ +{% extends "base.html" %} + +{% block content %} +
+

Login

+ + {% if error %} + + {% endif %} + +
+
+ + +
+
+ + +
+ +
+ +
+{% endblock %} diff --git a/templates/navbar.html b/templates/navbar.html new file mode 100644 index 0000000..a9253f0 --- /dev/null +++ b/templates/navbar.html @@ -0,0 +1,31 @@ +{% macro navbar() %} + +{% set pages = [ + {"endpoint": "root_page", "label": "Home" }, + {"endpoint": "login_page", "label": "Login" }, +] %} +{% set project = "Auth" %} + + + + +{% endmacro %}