Flask App SSL / CORS Configuration Walkthrough

This guide goes over detailed specifications of openssl, flask, python, pycharm with flask, ssl, certificates, certbot, and CORS to start. It finishes with a fully running registration, login, logout basic system that can be templated into large systems.

Flask App SSL / CORS Configuration Walkthrough

CORS - Cross Origin Resource Sharing.

SSL - Secure Socket Layer using TLS (Transport Layer Security)

There are a LOT of moving parts involved in a full flask api /  cookie / ssl / CORS arrangement and that can be daunting to understand even if you are a seasoned developer.  To simplify this as much as possible we will start with the simplest http static connection and build up from there, adding url parts, html static parts, then  the SSL,  the cookies, adding finally adding the CORS with login / logout and POST/GET/OPTION handlers.

If that is not enough 'gotchas' there is also the special configuration inherit to Pycharm (if you are using Pro) in terms of setting up the Flask environment. I cannot applaude their documentation it is very summary and could be greatly improved upon.

Simplest Example (http connection - no SSL / no cookies / only static GET requests)

  • It must be understood that a standard 'GET' is CORS and configuration exempt.
  • This is a static serving example. Because there are no forms or logins  (and no SSL encryption) there is no requirement for cookies, or CORS, or ca-certificates. So:

Part A. Pycharm Configuration and Gotchas

  1. Make a new application select the Flask type as in:

 It will produce the following template code:

from flask import Flask

app = Flask(__name__)


@app.route('/')
def hello_world():  # put application's code here
    return 'Hello World!'


if __name__ == '__main__':
    app.run()

If you run it by simply right clicking on it and selecting 'Run' pycharm will automatically generate the following in the console:

Here is where it starts throwing you curve balls even in this basic example, lets presume that you setup your code to bind to a different address/port as per example:

if __name__ == '__main__':
    app.run(host='192.168.1.62', port=5001, debug=True, ssl_context='adhoc')

When you run your application it ignores it and pushes it right out 127.0.0.1:5000. Why?

The answer lies a semi-hidden (because this is so poorly documented in general) run configuration you will need to set:

Under your run configurations:

It must be set in 'Additional Options' as in:

In order to get 'Additional Options' select 'Modify Options'

Then 'turn on additional options' and you will see the additional field box.

  • Even more confusing if you simply 'right click' 'debug' it will create a new generic 127.0.0.1:5000 as in:
  • So you must actually select that particularly modified run configuration that you have built at the top of the screen.
  • You can actually see the secondary run configuration that will 'push back' towards the 127.0.0.1:5000 Flask (app.py) In this instance I have the proper 'FlaskProject' showing the options required.

You can see it is setup correctly now on your console:

Because there is no certs, no cookies, no ssl, no CORS (and not even a web page) you quickly get a nice text reply:

Part B. Serving a static page.

  • Next we want to show the basic example of serving a static page say index.html.
  • Flask has a dedicated folder for static pages named.. well 'static' so:

The modified python looks as follows:

from flask import Flask, send_from_directory
import cryptography

app = Flask(__name__)


@app.route('/')
def index():
    return send_from_directory('static', 'index.html')

if __name__ == '__main__':
    app.run(host='192.168.1.62', port=5001, debug=True)

And our nice html page will look as follows:

''' <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Welcome</title>
    <script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100 min-h-screen flex items-center justify-center">
    <div class="bg-white p-8 rounded-lg shadow-lg max-w-md w-full text-center">
        <h1 class="text-3xl font-bold text-blue-600 mb-4">Welcome to the Application</h1>
        <p class="text-gray-700">This is a professional dashboard entry point. Explore further functionalities as needed.</p>
    </div>
</body>
</html> '''  

You should get a nice end point, as in:

Part C. Adding 'adhoc' Certificates

  • To add adhoc ssl you simply modify your pycharm run configuration as:
--host=192.168.1.62 --port=5001 --cert=adhoc

Naturally if done from inside the code it would look as:

if __name__ == '__main__':
    app.run(host='192.168.1.62', port=5001, debug=True, ssl_context='adhoc')
  • SSL Certificates are 'chained' meaning that if they are not linked all the way back to a root certificate they will produce errors, your browser will call out your 'home-brew' certificate with a warning, which you need to select 'Advanced' and then proceed.

It will serve the page with flags that effectively this is not 'true' SSL, but it's okay for development.

Part D. Generating  LOCAL  Certificates for SSL.

  • It must be understood these are not global 'DOMAIN' level certificates, but just read about them and from here it is then it is a small step to make a global certificate.
  • You will need to install openssl as in:
sudo apt update; sudo apt install openssl -y

Make sure it works with:

openssl

You should see something like:

Great! Now we will work on generating a local certificate here is an example:

openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 365 -nodes

Explanation of Options:

  • req: Initiates a certificate request.
  • -x509: Outputs a self-signed certificate instead of a certificate request.
  • -newkey rsa:4096: Generates a new RSA private key with 4096 bits (a secure key size).
  • -keyout key.pem: Specifies the output file for the private key.
  • -out cert.pem: Specifies the output file for the certificate.
  • -sha256: Uses SHA-256 for hashing (recommended for security).
  • -days 365: Sets the certificate validity period to 365 days.
  • -nodes: Disables passphrase protection on the private key

cert.pem - is your public certificate, that is shared out.

key.pem - is your private certificate, that is never shared out.

To setup your run configurations to handle these certificates via the run configuration is as:

--host=192.168.1.62 --port=5000 --cert=cert.pem --key=key.pem

Or from inside your application as:

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True, ssl_context=('cert.pem', 'key.pem'))

You would naturally add them to the folder where your main app.py sits.

We let Grok write this portion for a professional certificate:

Part E. Process of Using Certbot to Install Professional Domain Name Certificates

Certbot is the official client provided by the Electronic Frontier Foundation (EFF) for obtaining and managing free SSL/TLS certificates from Let's Encrypt, a trusted Certificate Authority (CA). This tool automates the issuance and installation of certificates for domain names, ensuring secure HTTPS connections. The process is particularly suited for Linux-based servers and supports common web servers such as Apache and Nginx. Below is a structured explanation of the steps involved, assuming a Linux environment with root or sudo access.

Prerequisites

Prior to initiating the process, verify the following requirements are met:

  • A registered domain name with DNS records (e.g., A records) pointing to the server's public IP address.
  • A functional web server (Apache or Nginx) configured for the domain.
  • Port 80 (HTTP) open on the server firewall to facilitate domain validation via the HTTP-01 challenge.
  • Administrative privileges on the Linux system.
  • Compatibility with supported distributions, such as Ubuntu, Debian, CentOS, or Fedora.

Step 1: Install Certbot

Certbot is installed via the system's package manager. Select the method corresponding to your distribution.

For Ubuntu or Debian-based systems:

sudo apt update
sudo apt install certbot python3-certbot-apache python3-certbot-nginx

For CentOS or RHEL-based systems:

sudo yum install epel-release
sudo yum install certbot python3-certbot-apache python3-certbot-nginx

For Fedora:

sudo dnf install certbot python3-certbot-apache python3-certbot-nginx

After installation, confirm the version:

certbot --version

This step also installs plugins for automatic configuration with Apache or Nginx.

Step 2: Obtain and Install the Certificate

Certbot provides options for manual or automated certificate handling. Specify your domain(s) using the -d flag (e.g., -d example.com -d www.example.com).

Manual Mode (Using certonly): Ideal for custom configurations without altering the web server setup.

sudo certbot certonly --standalone -d example.com -d www.example.com
  • This uses Certbot's temporary web server for validation.
  • Certificates are stored in /etc/letsencrypt/live/example.com/, including fullchain.pem (certificate chain) and privkey.pem (private key).
  • Manually update your web server configuration to reference these files and restart the server.

Part F. Modified Flask Application Code with CORS (For Full Domain Specification)

  • To incorporate Cross-Origin Resource Sharing (CORS) support, the Flask application has been updated to include the flask_cors extension. This enables controlled access to resources from different origins, which is essential for scenarios involving client-side requests from domains other than the server's origin. The configuration allows requests from any origin ("*") for development purposes, supports common HTTP methods, and permits credentials (e.g., cookies) if required. In production, it is advisable to restrict origins to specific trusted domains for enhanced security.

First, ensure the flask_cors package is installed by executing the following command in your terminal:

pip install flask-cors

The updated code is as follows:

from flask import Flask, send_from_directory
from flask_cors import CORS

app = Flask(__name__)

# Enable CORS with a basic configuration for all routes
CORS(app, resources={r"/*": {
    "origins": ["*"],  # Allow all origins; restrict in production (e.g., ["https://example.com"])
    "methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
    "allow_headers": ["Content-Type", "Authorization"],
    "supports_credentials": True
}})

@app.route('/')
def index():
    return send_from_directory('static', 'index.html')

if __name__ == '__main__':
    app.run(host='192.168.1.62', port=5001, debug=True)

Explanation of Changes

  • Import and Initialization: The CORS class is imported from flask_cors and initialized with the Flask application instance. The resources parameter specifies a pattern (r"/*") to apply CORS rules to all endpoints.
  • CORS Rules:
  • "origins": ["*"]: Permits requests from any domain during development. For security, replace "*" with an array of allowed origins (domains) in production.
  • "methods": Lists supported HTTP methods to accommodate common API operations.
  • "allow_headers": Specifies permitted request headers, such as those for content type or authentication.
  • "supports_credentials": True: Enables the inclusion of credentials (e.g., cookies or HTTP authentication) in cross-origin requests, if applicable.
  • Security Considerations: While this setup facilitates development, refine the origins and headers in a production environment to mitigate potential vulnerabilities like unauthorized access.

Upon running the application, CORS headers (e.g., Access-Control-Allow-Origin) will be included in responses, allowing compliant cross-origin interactions. If specific adjustments to the CORS policy are required, provide further details for refinement.

The index.html will look as, but please note:

  • The following code template it is important to use relative urls, per example:
  • The /generate_chat_url will be appended onto the domain name wherever the app lives which is served from the browser accessing 192.168.1.63:5000/index.html or in a FQDN www.somewhere.com/index.html thus /generate_chat_url is relative to this and that is all that is put into the javascript.
                const response = await fetch('/generate_chat_url', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ site_url: siteUrl }),
                    credentials: 'include'
                });
```<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Chat Board</title>
    <script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100 flex items-center justify-center min-h-screen">
    <div class="bg-white p-6 rounded-lg shadow-lg w-full max-w-md">
        <h1 class="text-2xl font-bold mb-4 text-center">Chat Board</h1>
        <div id="auth-section" class="space-y-4">
            <!-- Login Form -->
            <div id="login-form">
                <h2 class="text-lg font-semibold text-gray-800">Sign In</h2>
                <div class="space-y-2">
                    <div>
                        <label for="login-username" class="block text-sm font-medium text-gray-700">Username</label>
                        <input type="text" id="login-username" class="mt-1 block w-full p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" required>
                    </div>
                    <div>
                        <label for="login-password" class="block text-sm font-medium text-gray-700">Password</label>
                        <input type="password" id="login-password" class="mt-1 block w-full p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" required>
                    </div>
                    <button onclick="login()" class="w-full bg-blue-500 text-white p-2 rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500">Sign In</button>
                </div>
                <p class="text-sm text-gray-600 mt-2">No account? <a href="#" onclick="showRegister()" class="text-blue-500 underline">Register</a></p>
            </div>
            <!-- Register Form -->
            <div id="register-form" class="hidden">
                <h2 class="text-lg font-semibold text-gray-800">Register</h2>
                <div class="space-y-2">
                    <div>
                        <label for="register-username" class="block text-sm font-medium text-gray-700">Username</label>
                        <input type="text" id="register-username" class="mt-1 block w-full p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" required>
                    </div>
                    <div>
                        <label for="register-password" class="block text-sm font-medium text-gray-700">Password</label>
                        <input type="password" id="register-password" class="mt-1 block w-full p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" required>
                    </div>
                    <button onclick="register()" class="w-full bg-blue-500 text-white p-2 rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500">Register</button>
                </div>
                <p class="text-sm text-gray-600 mt-2">Already have an account? <a href="#" onclick="showLogin()" class="text-blue-500 underline">Sign In</a></p>
            </div>
        </div>
        <!-- Chat URL Form (hidden until logged in) -->
        <div id="chat-form" class="space-y-4 mt-4 hidden">
            <div>
                <label for="siteUrl" class="block text-sm font-medium text-gray-700">Site URL</label>
                <input type="url" id="siteUrl" placeholder="127.0.0.1" class="mt-1 block w-full p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" required>
            </div>
            <button onclick="generateChatUrl()" class="w-full bg-blue-500 text-white p-2 rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500">Generate Chat URL</button>
            <button onclick="logout()" class="w-full bg-red-500 text-white p-2 rounded-md hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500">Log Out</button>
        </div>
        <div id="response" class="mt-4 text-sm text-gray-700 space-y-2"></div>
    </div>

    <script>
        function showLogin() {
            document.getElementById('login-form').classList.remove('hidden');
            document.getElementById('register-form').classList.add('hidden');
            document.getElementById('response').innerHTML = '';
        }

        function showRegister() {
            document.getElementById('register-form').classList.remove('hidden');
            document.getElementById('login-form').classList.add('hidden');
            document.getElementById('response').innerHTML = '';
        }

        async function register() {
            const username = document.getElementById('register-username').value;
            const password = document.getElementById('register-password').value;
            const responseDiv = document.getElementById('response');

            if (!username || !password) {
                responseDiv.innerHTML = '<p class="text-red-500 font-medium">Please enter username and password.</p>';
                return;
            }

            try {
                const response = await fetch('/register', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ username, password }),
                    credentials: 'include'
                });

                const contentType = response.headers.get('Content-Type');
                if (!response.ok) {
                    let errorMessage;
                    if (contentType && contentType.includes('application/json')) {
                        const data = await response.json();
                        errorMessage = data.error || 'Unknown error';
                    } else {
                        const text = await response.text();
                        errorMessage = text.substring(0, 200); // Truncate for display
                        console.error('Non-JSON response:', text);
                    }
                    responseDiv.innerHTML = `<p class="text-red-500 font-medium">Error: ${errorMessage}</p>`;
                    return;
                }

                const data = await response.json();
                responseDiv.innerHTML = '<p class="text-green-500 font-medium">Registration successful! Please sign in.</p>';
                showLogin();
            } catch (error) {
                responseDiv.innerHTML = `<p class="text-red-500 font-medium">Error: Failed to connect to the server. ${error.message}</p>`;
                console.error(error);
            }
        }

        async function login() {
            const username = document.getElementById('login-username').value;
            const password = document.getElementById('login-password').value;
            const responseDiv = document.getElementById('response');

            if (!username || !password) {
                responseDiv.innerHTML = '<p class="text-red-500 font-medium">Please enter username and password.</p>';
                return;
            }

            try {
                const response = await fetch('/login', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ username, password }),
                    credentials: 'include'
                });

                const contentType = response.headers.get('Content-Type');
                if (!response.ok) {
                    let errorMessage;
                    if (contentType && contentType.includes('application/json')) {
                        const data = await response.json();
                        errorMessage = data.error || 'Unknown error';
                    } else {
                        const text = await response.text();
                        errorMessage = text.substring(0, 200);
                        console.error('Non-JSON response:', text);
                    }
                    responseDiv.innerHTML = `<p class="text-red-500 font-medium">Error: ${errorMessage}</p>`;
                    return;
                }

                const data = await response.json();
                document.getElementById('auth-section').classList.add('hidden');
                document.getElementById('chat-form').classList.remove('hidden');
                responseDiv.innerHTML = `<p class="text-green-500 font-medium">Welcome, ${data.user}!</p>`;
            } catch (error) {
                responseDiv.innerHTML = `<p class="text-red-500 font-medium">Error: Failed to connect to the server. ${error.message}</p>`;
                console.error(error);
            }
        }

        async function logout() {
            const responseDiv = document.getElementById('response');

            try {
                const response = await fetch('/logout', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    credentials: 'include'
                });

                const contentType = response.headers.get('Content-Type');
                if (!response.ok) {
                    let errorMessage;
                    if (contentType && contentType.includes('application/json')) {
                        const data = await response.json();
                        errorMessage = data.error || 'Unknown error';
                    } else {
                        const text = await response.text();
                        errorMessage = text.substring(0, 200);
                        console.error('Non-JSON response:', text);
                    }
                    responseDiv.innerHTML = `<p class="text-red-500 font-medium">Error: ${errorMessage}</p>`;
                    return;
                }

                const data = await response.json();
                document.getElementById('auth-section').classList.remove('hidden');
                document.getElementById('chat-form').classList.add('hidden');
                responseDiv.innerHTML = '<p class="text-green-500 font-medium">Logged out successfully.</p>';
                showLogin();
                window.location.reload();
            } catch (error) {
                responseDiv.innerHTML = `<p class="text-red-500 font-medium">Error: Failed to connect to the server. ${error.message}</p>`;
                console.error(error);
            }
        }

        async function generateChatUrl() {
            const siteUrl = document.getElementById('siteUrl').value;
            const responseDiv = document.getElementById('response');

            if (!siteUrl) {
                responseDiv.innerHTML = '<p class="text-red-500 font-medium">Please enter a valid URL.</p>';
                return;
            }

            try {
                const response = await fetch('/generate_chat_url', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ site_url: siteUrl }),
                    credentials: 'include'
                });

                const contentType = response.headers.get('Content-Type');
                if (!response.ok) {
                    let errorMessage;
                    if (contentType && contentType.includes('application/json')) {
                        const data = await response.json();
                        errorMessage = data.error || 'Unknown error';
                    } else {
                        const text = await response.text();
                        errorMessage = text.substring(0, 200);
                        console.error('Non-JSON response:', text);
                    }
                    responseDiv.innerHTML = `<p class="text-red-500 font-medium">Error: ${errorMessage}</p>`;
                    return;
                }

                const data = await response.json();
                responseDiv.innerHTML = `
                    <p class="text-lg font-semibold text-gray-800">Generated Chat URL:</p>
                    <p><a href="${data.chat_url}" class="text-blue-600 text-lg font-medium underline hover:text-blue-800" target="_blank">${data.chat_url}</a></p>
                    <p class="text-sm text-gray-600">Site ID: ${data.site_id}</p>
                `;
            } catch (error) {
                responseDiv.innerHTML = `<p class="text-red-500 font-medium">Error: Failed to connect to the server. ${error.message}</p>`;
                console.error(error);
            }
        }
    </script>
</body>
</html>```

This you would put inside your 'static' folder and name it index.html (take out the triple quotes they are there so ghost blog does not get tripped up!)

The app.py would look as:

  • Note this  uses 'CORS' specifcation per-url, you can most likely group these.
from flask import Flask, jsonify, request, url_for, make_response, send_from_directory
from flask_cors import CORS
from flask_bcrypt import Bcrypt
from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user
from urllib.parse import quote
from flask_sqlalchemy import SQLAlchemy
import logging
import cryptography

app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'  # Replace with a secure random key
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///chatboard.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SESSION_COOKIE_SAMESITE'] = None  # Allow cross-origin cookies
app.config['SESSION_COOKIE_SECURE'] = False  # Allow HTTP for local testing
app.config['SESSION_COOKIE_HTTPONLY'] = True

# Set up logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

# Initialize extensions
db = SQLAlchemy(app)
bcrypt = Bcrypt(app)
login_manager = LoginManager(app)
login_manager.login_view = 'login'

# Configure CORS to allow PyCharm's server and same-origin
#CORS(app, resources={r"/*": {"origins": ["http://192.168.1.62:5000", "http://localhost:63342"]}}, supports_credentials=True)
CORS(app, resources={r"/*": {
    "origins": ["*"],
    "allow_headers": ["Content-Type", "x-ijt"],  # Add 'x-ijt' or use '*' for all headers (less secure)
    "supports_credentials": True
}})


# User model
class User(db.Model, UserMixin):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    password_hash = db.Column(db.String(120), nullable=False)

# ChatMessage model
class ChatMessage(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    site_id = db.Column(db.String(50), nullable=False)
    message = db.Column(db.Text, nullable=False)
    level = db.Column(db.Integer, nullable=False)

# Flask-Login user loader
@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))

# Override unauthorized response to return JSON
@login_manager.unauthorized_handler
def unauthorized():
    logger.warning("Unauthorized access attempt")
    response = jsonify({'error': 'Unauthorized: Please log in'})
    response.headers['Content-Type'] = 'application/json'
    response.headers['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*')
    response.headers['Access-Control-Allow-Credentials'] = 'true'
    return response, 401

# Initialize database
with app.app_context():
    db.create_all()

# Placeholder generate_site_id function (replace with your implementation)
def generate_site_id():
    import uuid
    return str(uuid.uuid4())


@app.route('/index.html')
def index():
    return send_from_directory('static', 'index.html')

# Placeholder chat_widget route (replace with your actual route)
@app.route('/chat/<site_id>')
def chat_widget(site_id):
    response = jsonify({'message': f'Chat widget for site_id: {site_id}'})
    response.headers['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*')
    response.headers['Access-Control-Allow-Credentials'] = 'true'
    return response

# Serve HTML page
@app.route('/')
def serve_html():
    logger.debug("Serving index.html")
    return app.send_static_file('index.html')

# Registration endpoint
@app.route('/register', methods=['POST', 'OPTIONS'])
def register():
    if request.method == 'OPTIONS':
        logger.debug("Handling OPTIONS request for /register, Origin: %s", request.headers.get('Origin'))
        response = make_response('')
        response.headers['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*')
        response.headers['Access-Control-Allow-Methods'] = 'POST, OPTIONS'
        response.headers['Access-Control-Allow-Headers'] = 'Content-Type'
        response.headers['Access-Control-Allow-Credentials'] = 'true'
        return response, 200
    logger.debug("Handling POST request for /register")
    try:
        data = request.json
        username = data.get('username')
        password = data.get('password')

        if not username or not password:
            logger.error("Missing username or password")
            response = jsonify({'error': 'Username and password are required'})
            response.headers['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*')
            response.headers['Access-Control-Allow-Credentials'] = 'true'
            response.headers['Content-Type'] = 'application/json'
            return response, 400

        if User.query.filter_by(username=username).first():
            logger.error("Username already exists: %s", username)
            response = jsonify({'error': 'Username already exists'})
            response.headers['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*')
            response.headers['Access-Control-Allow-Credentials'] = 'true'
            response.headers['Content-Type'] = 'application/json'
            return response, 400

        password_hash = bcrypt.generate_password_hash(password).decode('utf-8')
        new_user = User(username=username, password_hash=password_hash)
        db.session.add(new_user)
        db.session.commit()
        login_user(new_user)
        logger.info("User registered: %s", username)
        response = jsonify({'message': 'User registered successfully'})
        response.headers['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*')
        response.headers['Access-Control-Allow-Credentials'] = 'true'
        response.headers['Content-Type'] = 'application/json'
        return response, 201
    except Exception as e:
        logger.error("Registration error: %s", str(e))
        response = jsonify({'error': f'Server error: {str(e)}'})
        response.headers['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*')
        response.headers['Access-Control-Allow-Credentials'] = 'true'
        response.headers['Content-Type'] = 'application/json'
        return response, 500

# Login endpoint
@app.route('/login', methods=['POST', 'OPTIONS'])
def login():
    if request.method == 'OPTIONS':
        logger.debug("Handling OPTIONS request for /login, Origin: %s", request.headers.get('Origin'))
        response = make_response('')
        response.headers['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*')
        response.headers['Access-Control-Allow-Methods'] = 'POST, OPTIONS'
        response.headers['Access-Control-Allow-Headers'] = 'Content-Type'
        response.headers['Access-Control-Allow-Credentials'] = 'true'
        return response, 200
    logger.debug("Handling POST request for /login")
    try:
        data = request.json
        username = data.get('username')
        password = data.get('password')

        user = User.query.filter_by(username=username).first()
        if user and bcrypt.check_password_hash(user.password_hash, password):
            login_user(user)
            logger.info("User logged in: %s", username)
            response = jsonify({'message': 'Login successful', 'user': username})
            response.headers['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*')
            response.headers['Access-Control-Allow-Credentials'] = 'true'
            response.headers['Content-Type'] = 'application/json'
            return response, 200
        logger.error("Invalid login attempt for username: %s", username)
        response = jsonify({'error': 'Invalid username or password'})
        response.headers['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*')
        response.headers['Access-Control-Allow-Credentials'] = 'true'
        response.headers['Content-Type'] = 'application/json'
        return response, 401
    except Exception as e:
        logger.error("Login error: %s", str(e))
        response = jsonify({'error': f'Server error: {str(e)}'})
        response.headers['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*')
        response.headers['Access-Control-Allow-Credentials'] = 'true'
        response.headers['Content-Type'] = 'application/json'
        return response, 500

# Logout endpoint
@app.route('/logout', methods=['POST', 'OPTIONS'])
@login_required
def logout():
    if request.method == 'OPTIONS':
        logger.debug("Handling OPTIONS request for /logout, Origin: %s", request.headers.get('Origin'))
        response = make_response('')
        response.headers['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*')
        response.headers['Access-Control-Allow-Methods'] = 'POST, OPTIONS'
        response.headers['Access-Control-Allow-Headers'] = 'Content-Type'
        response.headers['Access-Control-Allow-Credentials'] = 'true'
        return response, 200

    logger.debug("Handling POST request for /logout")
    try:
        logout_user()
        logger.info("User logged out")
        response = jsonify({'message': 'Logged out successfully'})
        response.headers['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*')
        response.headers['Access-Control-Allow-Credentials'] = 'true'
        response.headers['Content-Type'] = 'application/json'
        return response, 200
    except Exception as e:
        logger.error("Logout error: %s", str(e))
        response = jsonify({'error': f'Server error: {str(e)}'})
        response.headers['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*')
        response.headers['Access-Control-Allow-Credentials'] = 'true'
        response.headers['Content-Type'] = 'application/json'
        return response, 500

# Generate chat URL endpoint
@app.route('/generate_chat_url', methods=['POST'])  # Remove 'OPTIONS'
@login_required
def generate_chat_url():
    logger.debug("Handling POST request for /generate_chat_url")
    try:
        site_url = request.json.get('site_url')
        if not site_url:
            logger.error("Missing site_url")
            response = jsonify({'error': 'Site URL is required'})
            response.headers['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*')
            response.headers['Access-Control-Allow-Credentials'] = 'true'
            response.headers['Content-Type'] = 'application/json'
            return response, 400

        site_id = generate_site_id()
        encoded_site_url = quote(site_url, safe='')
        custom_url = url_for('chat_widget', site_id=site_id, _external=True)

        with app.app_context():
            new_message = ChatMessage(site_id=site_id, message=f"Initialized chat for {site_url}", level=0)
            db.session.add(new_message)
            db.session.commit()

        logger.info("Chat URL generated for site_id: %s", site_id)
        response = jsonify({'chat_url': custom_url, 'site_id': site_id})
        response.headers['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*')
        response.headers['Access-Control-Allow-Credentials'] = 'true'
        response.headers['Content-Type'] = 'application/json'
        return response
    except Exception as e:
        logger.error("Generate chat URL error: %s", str(e))
        response = jsonify({'error': f'Server error: {str(e)}'})
        response.headers['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*')
        response.headers['Access-Control-Allow-Credentials'] = 'true'
        response.headers['Content-Type'] = 'application/json'
        return response, 500


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True, ssl_context=('cert.pem','key.pem'))
    #app.run(host='0.0.0.0', port=5000, debug=True, ssl_context='adhoc')

When you run this, it will appear as follows:

As it works you will see that the back-end is serving and processing pages, when we do a 'registration' it shows up as:

And we can now test a login:

Proper URLs are generated for the 'chat board'

Understanding some important portions of the code:

  • flask has a 'logged in' / 'session manager'
  • Note @login_required.
# Generate chat URL endpoint
@app.route('/generate_chat_url', methods=['POST'])  # Remove 'OPTIONS'
@login_required
def generate_chat_url():
    logger.debug("Handling POST request for /generate_chat_url")
    try:
        site_url = request.json.get('site_url')
        if not site_url:
            logger.error("Missing site_url")
            response = jsonify({'error': 'Site URL is required'})
            response.headers['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*')
            response.headers['Access-Control-Allow-Credentials'] = 'true'
            response.headers['Content-Type'] = 'application/json'
            return response, 400

        site_id = generate_site_id()

We should also note that some replies can have two -parts, an 'OPTIONS' part, and the main 'RESPONSE' part.

  • OPTIONS will inform the browser what they are allowed to do as in:
    if request.method == 'OPTIONS':
        logger.debug("Handling OPTIONS request for /register, Origin: %s", request.headers.get('Origin'))
        response = make_response('')
        response.headers['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*')
        response.headers['Access-Control-Allow-Methods'] = 'POST, OPTIONS'
        response.headers['Access-Control-Allow-Headers'] = 'Content-Type'
        response.headers['Access-Control-Allow-Credentials'] = 'true'
        return response, 200
  • This is effectively stating, you can POST, you are allowed ANY origin '*', and you can allow-crediential.
  • This response is returned before the main body is returned.

The main REPONSE then is in its own try/except block

    try:
        data = request.json
        username = data.get('username')
        password = data.get('password')

        if not username or not password:
            logger.error("Missing username or password")
            response = jsonify({'error': 'Username and password are required'})
            response.headers['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*')
            response.headers['Access-Control-Allow-Credentials'] = 'true'
            response.headers['Content-Type'] = 'application/json'
            return response, 400

        if User.query.filter_by(username=username).first():
            logger.error("Username already exists: %s", username)
            response = jsonify({'error': 'Username already exists'})
            response.headers['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*')
            response.headers['Access-Control-Allow-Credentials'] = 'true'
            response.headers['Content-Type'] = 'application/json'
            return response, 400

        password_hash = bcrypt.generate_password_hash(password).decode('utf-8')
        new_user = User(username=username, password_hash=password_hash)
        db.session.add(new_user)
        db.session.commit()
        login_user(new_user)
        logger.info("User registered: %s", username)
        response = jsonify({'message': 'User registered successfully'})
        response.headers['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*')
        response.headers['Access-Control-Allow-Credentials'] = 'true'
        response.headers['Content-Type'] = 'application/json'
        return response, 201
    except Exception as e:
        logger.error("Registration error: %s", str(e))
        response = jsonify({'error': f'Server error: {str(e)}'})
        response.headers['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*')
        response.headers['Access-Control-Allow-Credentials'] = 'true'
        response.headers['Content-Type'] = 'application/json'
        return response, 500

This is fed back to the await async function of the javascript, which handles and informs back to the user if it went:

        async function register() {
            const username = document.getElementById('register-username').value;
            const password = document.getElementById('register-password').value;
            const responseDiv = document.getElementById('response');

            if (!username || !password) {
                responseDiv.innerHTML = '<p class="text-red-500 font-medium">Please enter username and password.</p>';
                return;
            }

            try {
                const response = await fetch('/register', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ username, password }),
                    credentials: 'include'
                });

                const contentType = response.headers.get('Content-Type');
                if (!response.ok) {
                    let errorMessage;
                    if (contentType && contentType.includes('application/json')) {
                        const data = await response.json();
                        errorMessage = data.error || 'Unknown error';
                    } else {
                        const text = await response.text();
                        errorMessage = text.substring(0, 200); // Truncate for display
                        console.error('Non-JSON response:', text);
                    }
                    responseDiv.innerHTML = `<p class="text-red-500 font-medium">Error: ${errorMessage}</p>`;
                    return;
                }

                const data = await response.json();
                responseDiv.innerHTML = '<p class="text-green-500 font-medium">Registration successful! Please sign in.</p>';
                showLogin();
            } catch (error) {
                responseDiv.innerHTML = `<p class="text-red-500 font-medium">Error: Failed to connect to the server. ${error.message}</p>`;
                console.error(error);
            }
        }

We will stop here, that was a LOT of information, and a LOT of moving parts just for a secure reliable session enabled login/registration type site. However this can be built from this basic template into anything, and that is really powerful.

Additionally you can monitor the progress debugging from the web side by simply enabling a debug session from the browser side, right click, use 'select' and click on the 'console' which will show errors to a web page.

  • It is common that literally no site is perfect, they will all have some errors in them.

You have covered a LOT of parts congratulate yourself if you got this far, you covered:

  • flask basics / static html pages
  • ssl local and global configurations (certbot and openssl)
  • cookies and sessions in the Flask context
  • pycharm configuration over its poor documentation.
Linux Rocks Every Day