Initial commit
This commit is contained in:
commit
49b228c1a5
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
__pycache__
|
||||
config
|
21
LICENSE
Normal file
21
LICENSE
Normal 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
73
README.md
Normal 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
103
main.py
Executable 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
8
templates/auth_good.html
Normal 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
23
templates/base.html
Normal 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
26
templates/login.html
Normal 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
31
templates/navbar.html
Normal 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 %}
|
Loading…
Reference in a new issue