Initial commit

This commit is contained in:
Edward Betts 2024-01-21 09:10:07 +00:00
commit 49b228c1a5
8 changed files with 287 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
__pycache__
config

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Edward Betts <edward@4angle.com>
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.

73
README.md Normal file
View file

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

103
main.py Executable file
View file

@ -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")

8
templates/auth_good.html Normal file
View file

@ -0,0 +1,8 @@
{% extends "base.html" %}
{% block content %}
<div class="container-fluid mt-2">
<h1>Auth</h1>
<p>Login successful.</p>
</div>
{% endblock %}

23
templates/base.html Normal file
View file

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}{% endblock %}</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>📅</text></svg>">
<link href="https://unpkg.com/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
{% block style %}
{% endblock %}
</head>
{% from "navbar.html" import navbar with context %}
<body>
{% block nav %}{{ navbar() }}{% endblock %}
{% block content %}{% endblock %}
{% block scripts %}{% endblock %}
<script src="https://unpkg.com/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
</body>
</html>

26
templates/login.html Normal file
View file

@ -0,0 +1,26 @@
{% extends "base.html" %}
{% block content %}
<div class="container-fluid mt-2">
<h1>Login</h1>
{% if error %}
<div class="alert alert-danger" role="alert">
{{ error }}
</div>
{% endif %}
<form method="POST">
<div class="mb-3">
<label for="username" class="form-label">username</label>
<input class="form-control" id="username" name="username">
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="passwod" name="password">
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
{% endblock %}

31
templates/navbar.html Normal file
View file

@ -0,0 +1,31 @@
{% macro navbar() %}
{% set pages = [
{"endpoint": "root_page", "label": "Home" },
{"endpoint": "login_page", "label": "Login" },
] %}
{% set project = "Auth" %}
<nav class="navbar navbar-expand-md bg-success" data-bs-theme="dark">
<div class="container-fluid">
<a class="navbar-brand" href="{{ url_for("root_page") }}">{{ project }}</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
{% for page in pages %}
{% set is_active = request.endpoint == page.endpoint %}
<li class="nav-item">
<a class="nav-link{% if is_active %} border border-white border-2 active{% endif %}" {% if is_active %} aria-current="page"{% endif %} href="{{ url_for(page.endpoint) }}">
{{ page.label }}
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
</nav>
{% endmacro %}