29 : Implementing Login using FastAPI and Jinja

In the previous post we implemented HttpOnly Cookie and tried to secure our web app. In this article, we are going to provide login functionality. Once someone logins in our web app, we would store an HttpOnly cookie in their browser which will be used to identify the user for future requests.
Lets first create a class which will act as form validator for us. Everytime any user submits a HTML form in the UI, It will get encapsulated in a POST request and we need to validate the input before trying to log them in. Create a new  file webapps > auth > forms.py and put the form logic in this file and try to understand:

from typing import List
from typing import Optional

from fastapi import Request


class LoginForm:
    def __init__(self, request: Request):
        self.request: Request = request
        self.errors: List = []
        self.username: Optional[str] = None
        self.password: Optional[str] = None

    async def load_data(self):
        form = await self.request.form()
        self.username = form.get(
            "email"
        )  # since outh works on username field we are considering email as username
        self.password = form.get("password")

    async def is_valid(self):
        if not self.username or not (self.username.__contains__("@")):
            self.errors.append("Email is required")
        if not self.password or not len(self.password) >= 4:
            self.errors.append("A valid password is required")
        if not self.errors:
            return True
        return False

Obviously, you can make the validation much better e.g. by validating the email using a Regex or by checking password constraints. But, here I am doing a simple validation in the is_valid method. If you have gone through Registration e.g. you would understand this pattern much more. I believe this class is self-explanatory. Moving forward, Now we need to handle the get and post request from users. A get request will ask for an empty form so, Its logic is pretty simple but for a post request we will be using LoginForm class to valid user inputs. Again, we need to create a new file webapps > auth > route_login.py

from apis.version1.route_login import login_for_access_token
from db.session import get_db
from fastapi import APIRouter
from fastapi import Depends
from fastapi import HTTPException
from fastapi import Request
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session
from webapps.auth.forms import LoginForm


templates = Jinja2Templates(directory="templates")
router = APIRouter(include_in_schema=False)


@router.get("/login/")
def login(request: Request):
    return templates.TemplateResponse("auth/login.html", {"request": request})


@router.post("/login/")
async def login(request: Request, db: Session = Depends(get_db)):
    form = LoginForm(request)
    await form.load_data()
    if await form.is_valid():
        try:
            form.__dict__.update(msg="Login Successful :)")
            response = templates.TemplateResponse("auth/login.html", form.__dict__)
            login_for_access_token(response=response, form_data=form, db=db)
            return response
        except HTTPException:
            form.__dict__.update(msg="")
            form.__dict__.get("errors").append("Incorrect Email or Password")
            return templates.TemplateResponse("auth/login.html", form.__dict__)
    return templates.TemplateResponse("auth/login.html", form.__dict__)

Woahh, this is a big one, but I feel the code is pretty straightforward. In the POST method we are basically,

  • Instantiating the LoginForm class
  • Setting up the class properties/attributes to user-inputted data.
  • Checking if the form is valid
  • If the form is valid we are creating a template response which we pass in one of our internal functions as it requires a response as a parameter. (See its implementation).
  • The internal function login_for_access_token validates if the username and password is correct.
  • If the username and password are correct we are providing an HttpOnly cookie to user's browser. So, that from the next request onwards they won't need to login again and again.
  • In case any HttpException occurs we are assuming that the username and password were incorrect and updating form.errors.

Time to have a template for our implementation. create a new file templates > auth > login.html

{% extends "shared/base.html" %}

{% block title %}
  <title>Login</title>
{% endblock %}

{% block content %}
  <div class="container">
    <div class="row">
      <h5 class="display-5">Login to JobBoard</h5>
      <div class="text-danger font-weight-bold">
          {% for error in errors %}
            <li>{{error}}</li>
          {% endfor %}
      </div>

      <div class="text-success font-weight-bold">
        {% if msg %}
        <div class="badge bg-success text-wrap font-weight-bold" style="font-size: large;">
          {{msg}}
        </div>
        {% endif %}
      </div>
    </div>


    <div class="row my-5">
      <form method="POST">
        <div class="mb-3">
          <label>Email</label>
          <input type="text" required placeholder="Your email" name="email" value="{{email}}" class="form-control">
        </div>
        <div class="mb-3">
          <label>Password</label>
          <input type="password" required placeholder="Choose a secure password" value="{{password}}" name="password" class="form-control">
        </div>
        <button type="submit" class="btn btn-primary">Submit</button>
      </form>
    </div>
  </div>
  {% endblock %}

All done, Now we need to inform the app in the main.py file. That we have created something like this. update webapps > base.py file

from fastapi import APIRouter
from webapps.auth import route_login   #new 
from webapps.jobs import route_jobs
from webapps.users import route_users


api_router = APIRouter()
api_router.include_router(route_jobs.router, prefix="", tags=["job-webapp"])
api_router.include_router(route_users.router, prefix="", tags=["users-webapp"])
api_router.include_router(route_login.router, prefix="", tags=["auth-webapp"])   #new

Time to test our implementation. Fingers crossed 🤞

Final git commit: https://github.com/nofoobar/JobBoard-Fastapi/commit/0e1bdf676a66e3f341d00c590b3fd07b269488f4

Prev: 28 : Securing … Next: 30 : Fastapi …