Token-Based Authentication With Inspira

In this guide you'll learn how to secure a Inspira application using JWT (JSON Web Tokens).

Getting Started

To begin, follow these commands to install Inspira and set up the project directories:

$ mkdir inspira-jwt
$ cd inspira-jwt
$ python -m venv .venv
$ source .venv/bin/activate
$ pip install inspira
$ inspira init
$ inspira new database --name mydb --type sqlite

Creating a Model

Create a new model named user by executing this command:

$ inspira new model user

Afterward, access the newly generated user model and make changes to the file as demonstrated below:

from inspira.auth.mixins.user_mixin import UserMixin
from sqlalchemy import Column, Integer, String
from database import Base


class User(Base, UserMixin):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True)
    email = Column(String(255), unique=True, nullable=False)
    password = Column(String(255), nullable=False)

Creating User Repository

Generate the user repository using the following command:

$ inspira new repository user

Next, open the user_repository.py file and apply the following modifications:

from sqlalchemy.exc import SQLAlchemyError

from database import db_session
from src.model.user import User


class UserRepository:

    def get_all_user(self):
        return db_session.query(User).all()

    def get_user_by_id(self, user_id: int) -> User:
        return db_session.query(User).filter_by(id=user_id).first()

    def get_user_by_email(self, email: str) -> User:
        return db_session.query(User).filter_by(email=email).first()

    def create_user(self, user: User) -> bool:
        try:
            db_session.add(user)
            db_session.commit()
            return True
        except SQLAlchemyError as e:
            db_session.rollback()
            print(f"Error creating user: {e}")
            return False

get_all_users: Retrieve all users from the database.
get_user_by_id: Retrieve a single user by their ID.
get_user_by_email: Retrieve a single user by their email address.
create_user: Save a new user in the database.

Creating User Service

Generate the user service with the command:

$ inspira new service user

Now, open the user_service.py file and update it to match the following:

from src.model.user import User
from src.repository.user_repository import UserRepository


class UserService:
    def __init__(self, user_repository: UserRepository):
        self._user_repository = user_repository

    def get_all_user(self):
        return self._user_repository.get_all_user()

    def get_user_by_id(self, id: int) -> User:
        return self._user_repository.get_user_by_id(id)

    def get_user_by_email(self, email) -> User:
        return self._user_repository.get_user_by_email(email)

    def create_user(self, name: str, email: str, password: str) -> bool:
        new_user = User()
        new_user.name = name
        new_user.email = email
        new_user.set_password(password)

        return self._user_repository.create_user(new_user)


  • get_all_users:
    Retrieve all users from the database.
  • get_user_by_id:
    Retrieve a single user by their ID.
  • get_user_by_email:
    Retrieve a single user by their email address.
  • create_user:
    Create a new user.

The UserService and UserRepository classes are vital for overseeing user-related operations in the application.
The UserService encapsulates and coordinates user-related business logic, interacting with the UserRepository to handle the underlying data storage and retrieval tasks.
This clear separation of concerns improves code modularity, maintainability, and testability in the application.

Creating Controller

Generate a new controller named user using the following command:

$ inspira new controller user

Now, open the user_controller.py file and structure it as illustrated below:

from inspira.auth.auth_utils import encode_auth_token, decode_auth_token
from inspira.decorators.http_methods import get, post
from inspira.decorators.path import path
from inspira.responses import JsonResponse
from inspira.requests import Request

from src.service.user_service import UserService


@path("/users")
class UserController:

    def __init__(self, user_service: UserService):
        self._user_service = user_service

    @get("/{id}")
    async def get_user(self, request: Request, id: int):
        auth_header = request.get_headers().get('authorization', '')
        auth_token = auth_header.split(" ")[1] if auth_header else ''

        if auth_token:
            decoded_data = decode_auth_token(auth_token)
            if decoded_data == id:
                user = self._user_service.get_user_by_id(decoded_data)
                context = {
                    "id": user.id,
                    "email": user.email
                }
                return JsonResponse(context)

        context = {
            'status': 'fail',
            'message': 'Provide a valid auth token.'
        }

        return JsonResponse(context, status_code=401)

    @post("/register")
    async def register_user(self, request: Request):
        body = await request.json()
        name = body['name']
        email = body['email']
        password = body['password']
        user = self._user_service.get_user_by_email(email)

        if not user:
            success = self._user_service.create_user(name, email, password)

            if success:
                return JsonResponse({"message": "User successfully registered."})
            else:
                return JsonResponse({"message": "Failed to register user"}, status_code=401)
        else:
            return JsonResponse({"message": "User already exists."})

    @post("/login")
    async def login(self, request: Request):
        body = await request.json()
        email = body['email']
        password = body['password']

        user = self._user_service.get_user_by_email(email)

        if user:
            if user.check_password_hash(password):
                auth_token = encode_auth_token(user.id)

                context = {
                    'auth_token': auth_token
                }

                return JsonResponse(context)
            else:
                return JsonResponse({"message": "Failed to login user"}, status_code=401)
        else:
            return JsonResponse({"message": "User not found"}, status_code=401)

  • get_user:
    Retrieves user information based on the provided user ID.
  • register:
    Handles the registration of a new user.
  • login:
    Manages the user login process.

Migrations

To initiate the creation of the users table, execute the following command to generate a migration file:

$ inspira new migration create_users_table

This command will create the migration file in the migrations folder. Open the file and make the necessary modifications as shown below:

CREATE TABLE users (
    id INTEGER NOT NULL, 
    email VARCHAR(255) NOT NULL, 
    password VARCHAR(255) NOT NULL, 
    PRIMARY KEY (id), 
    UNIQUE (email)
);

To apply the migration and create the user table, run the following command:

$ inspira migrate

Launching the Server

Start the server using the following command:

$ uvicorn main:app --reload

Postman

Register User:
Make a POST request to http://127.0.0.1:8000/users/register in Postman.
Use the following payload:

{
    "name": "Hayri Cicek",
    "email": "hayri.cicek@inspiraframework.com",
    "password": "secret1234"
}

Postman create user

Login and Retrieve Auth Token:

Make another POST request to http://127.0.0.1:8000/users/login with the following payload:

{
    "email": "hayri.cicek@inspiraframework.com",
    "password": "secret1234"
}

Postman create user

  • Copy the token

Retrieve User Information:
Make a GET request to http://127.0.0.1:8000/users/2 to retrieve user information.
In the Authorization tab, select Bearer type, paste the copied token, and send the request.
If everything works, you should see the user information.

Postman create user

Conclusion

In this tutorial, we went through the process of adding authentication to Inspira application with JWT.

The code is available on GitHub

Happy coding!