User Authentication

Simple Authentication without a User Table

If you just need a very simple authentication scheme to limit access to parts of your site (e.g. for an admin page), you can use just a password…

1. Add a password to your .env file

ADMIN_PASSWORD=secret

2. Create a login form page

Serve this up from /login route…

<form method="post" action="/login">
    <label>
        Password
        <input name="password" type="password" required>
    </label>

    <button>Login</button>
</form>

3. Create a login POST route

This loads the password from the .env file and compares it with the entered one…

@app.post("/login")
def login_user():
    password = request.form.get('password', '').strip()

    load_dotenv()
    ADMIN_PASSWORD = getenv("ADMIN_PASSWORD", "")
    if not ADMIN_PASSWORD:
        flash(f"No admin password set!", "error")
        return redirect("/")

    if password == ADMIN_PASSWORD:
        session["logged_in"] = True
        flash(f"Login successful", "success")
        return redirect("/")
    else:
        session.clear()
        flash(f"Incorrect password", "error")
        return redirect("/")

4. Create a logout route

@app.get("/logout")
def logout_user():
    session.clear()
    flash(f"You have been logged out", "success")
    return redirect("/")

Update the nav menu in templates/pages/base.jinja

{# Show different options depending on login state... #}

{% if session.logged_in %}
    Welcome, Admin!
    <a href="/logout">Logout</a>
{% else %}
    <a href="/login">Login</a>
{% endif %}     

6. Add the @login_required decorator to required routes

@app.get("/admin")
@login_required
def admin_page():
    # Can only access this route if logged in
    ...

User Authentication with User Account Table

If you need to create multiple user accounts and authenticate them based on username and password, you will need a dedicated user table in your database.

Note: Best practice is to never store user passwords in your database, but instead to store a hash of the password

1. Create a ‘user’ table

CREATE TABLE users (
    id        INTEGER PRIMARY KEY AUTOINCREMENT,
    forename  TEXT NOT NULL,
    surname   TEXT NOT NULL,
    username  TEXT NOT NULL UNIQUE,
    pass_hash TEXT NOT NULL
)

2. Create a sign-up form

Serve this from a /user/new route, or similar…

<form method="post" action="/user">
    <label>
        Forename
        <input name="forename" type="text" required>
    </label>

    <label>
        Surname
        <input name="surname" type="text" required>
    </label>

    <label>
        Username
        <input name="username" type="text" required>
    </label>

    <label>
        Password
        <input name="password" type="password" required>
    </label>

    <button>Sign Up</button>
</form>

3. Create a user POST route

Once this is in place, you should be able to create user accounts

@app.post("/user")
def add_user():
    forename = request.form.get('forename', '').strip()
    surname  = request.form.get('surname',  '').strip()
    username = request.form.get('username', '').strip().lower()
    password = request.form.get('password', '').strip()

    with connect_db() as db:
        sql = "SELECT id FROM users WHERE username=?"
        params = (username,)
        user = db.execute(sql, params).fetchone()

        if user:
            flash(f"Username '{username}' already exists", "error")
            return redirect("/user/new")

        pass_hash = generate_password_hash(password)

        sql = """
            INSERT INTO users (forename, surname, username, pass_hash)
            VALUES (?, ?, ?, ?)
        """
        params = (forename, surname, username, pass_hash)
        db.execute(sql, params)

        flash("Account created. Please login", "success")
        return redirect("/login")

4. Create a login form page

Serve this up from /login route…

<form method="post" action="/login">
    <label>
        Username
        <input name="username" type="text" required>
    </label>

    <label>
        Password
        <input name="password" type="password" required>
    </label>

    <button>Login</button>
</form>

5. Create a login POST route

This loads the password from the .env file and compares it with the entered one…

@app.post("/login")
def login_user():
    username = request.form.get('username', '').strip().lower()
    password = request.form.get('password', '').strip()

    with connect_db() as db:
        sql = """
            SELECT id, username, forename, surname, pass_hash
            FROM users
            WHERE username=?
        """
        params = (username,)
        user = db.execute(sql, params).fetchone()

        if not user:
            flash(f"Unknown user", "error")
            return redirect("/login")

        if not check_password_hash(user["pass_hash"], password):
            flash(f"Incorrect password", "error")
            return redirect("/login")

        session["logged_in"] = True
        session["user"] = {
            "id":       user["id"],
            "username": user["username"],
            "forename": user["forename"],
            "surname":  user["surname"],
        }

        flash("Login successful", "success")
        return redirect("/")

6. Create a logout route

@app.get("/logout")
def logout_user():
    session.clear()
    flash(f"You have been logged out", "success")
    return redirect("/")

Update the nav menu in templates/pages/base.jinja

{# Show different options depending on login state... #}

{% if session.logged_in %}
    Welcome, {{ session.user.forename }}!
    <a href="/logout">Logout</a>
{% else %}
    <a href="/login">Login</a>
    <a href="/user/new">Sign-Up</a>
{% endif %}     

8. Use the logged-in user info. when required

If you need to know the logged in user information (e.g. you need the ID when posting a new DB record linked to the user), you can get this from session["user"]

with connect_db() as db:
    user_id = session["user"]["id"]
    sql = """
        INSERT INTO notes (title, body, user_id)
        VALUES (?, ?, ?)
    """
    params = (title, body, user_id)
    db.execute(sql, params)

Or in a Jinja template (using the ‘dot’ syntax)…

{# Show logged in user name... #}

{% if session.logged_in %}
    <p>Welcome, {{ session.user.forename }}!</p>
{% endif %}     

9. Add the @login_required decorator to required routes

@app.get("/admin")
@login_required
def admin_page():
    # Can only access this route if logged in
    ...