Current Status

This is a rough article. Once I have free time, I will rewrite the entire article to use the proper technologies I used.

Preamble

I'm currently working on a project. Ideally, the model of user authentication should be the traditional username and password. However, I'm a lazy dev, and I do not want to have to deal with the complexity of that model.

I have used applications that employ the Single Sign On model, and I find it fascinating. Interestingly, I don't really have an idea of how it worked then. I thought of a simple way, and I think it's sufficient.

That said, I'll be writing a concise article on how I built an SSO model for a FastAPI Application in the future.

Working Principle

For this model, a token will be generated by a function, tied to the requested user and then registered in the database.

The purpose of saving this in the database is to act as a point of validation when the sign-in link is used. That is, whenever a sign-in link is used, the application checks if it is saved in the database before proceeding to give a response.

I have tried to demonstrate this in the image below:

Excaldiraw diagram of the working principle

I must apologize ahead: In this article, I used an in-memory database. However, the concept is the same as when applied to a real database.

Prerequisite

Scaffold your application

The first step is to create an empty directory and scaffold an application:

$ mkdir sso-app && cd sso-app
$ python3 -m venv

Activate the virtual environment and install the application dependencies:

$ . venv/bin/activate
(venv)$ pip install fastapi uvicorn
(venv)$ pip freeze > requirements.txt 

Create the routing file, and utilities file:

(venv)$ touch {routes,utilities}.py

Write the routes

I'll be having a route to:

In routes.py:

from fastapi import FastAPI

from utilities import generate_token

users = {}
login_tokens = []

router = FastAPI(
    title="SSO Application",
    description="Simple application demo on how SSO works"
)


@router.post("/new/")
async def new_user(user: dict):
    user_id = len(users) + 1
    users[user_id] = user

    return {
        "message": "User data successfully added"
    }


@router.post("/sso")
async def get_sso_link(user_id: int):
    if users[user_id]:
        token = await generate_token(user_id)
        login_tokens.append(token)
        return token
    return {
        "error": "User with supplied ID doesn't exist"
    }


@router.get("/sso/")
async def sign_in(token: str):
    if token in login_tokens:
        return {
            "message": "User logged in successfully."
        }

    return {
        "error": "Invalid token passed"
    }

Write the utility function

The utility function is named generate_token. It is responsible for generating identifiers.

In utilities.py:

import random
import string

async def generate_token(id: str):
    length_of_token = 12
    identifier = ""

    for i in range(length_of_token):
        identifier += random.choice(string.ascii_letters)
    return identifier

Start your application

(venv)$ uvicorn routes:router --host 0.0.0.0 --port 8080

Testing

  1. Add a new user:
$ curl -X 'POST' \
  'http://0.0.0.0:8080/new/' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "username": "Abdulazeez"
}'
  1. Get an SSO token
curl -X 'POST' \
  'http://0.0.0.0:8080/sso?user_id=1' \
  -H 'accept: application/json' \
  -d ''

The token returned is: KDZfHjQAdiFl

  1. Sign In
curl -X 'GET' \
  'http://0.0.0.0:8080/sso/?token=KDZfHjQAdiFl' \
  -H 'accept: application/json'

Response:

{
  "message": "User logged in successfully."
}
  1. Using a wrong token:
curl -X 'GET' \
  'http://0.0.0.0:8080/sso/?token=fKDZfHjQAdiFl' \
  -H 'accept: application/json'

Response:

{
  "error": "Invalid token passed"
}

Conclusion

A proper conclusion will be written once I have completed this draft. Until then, I'll say check back frequently.

References