Build simple authentication API using FAST API with ES256 encryption in 10 mins

This article covered the following topics:

  1. Creating a FAST API project and install PyJWT package.
  2. Create and validate ES256 JWT token.
  3. Validation by accessing the protected endpoints with JWT token
  4. Build this project into a container image for microservice.

Technology used

  1. FastAPI for serving authentication API with asynchronous pattern.
  2. PyJWT for generating and validating ES256 JWT token.
  3. JWT dashboard for visualising the token we created.
  4. OpenAPI(previously known as swagger) for documenting API specification.
  5. Docker for building container image.
  6. Postman

Getting Started

Step 1: Creating a FAST API project and install PyJWT package.

# Create new directory for this project
mkdir auth-fastapi
cd auth-fastapi
# Install the fastapi module
pip install fastapi
pip install uvicorn[standard]

Define root endpoint

from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello World"}

Run the project

uvicorn main:app --reload

Install PyJWT python module

pip install pyjwt

Step 2: Create and validate ES256 JWT token.

  1. /login endpoint to login user and generate token for the user.
  2. /secret endpoint, only user with validate token can access this endpoint.

Generate ES256 public and private key set in JSON Web Key(JWK) format

From https://mkjwk.org/
# example 
{
"keys": [
{
"kty": "EC",
"d": "jwrAjoo8YOk-ryUgQtarw4P-id8c3BD0jOoWJ3PgNyU",
"use": "sig",
"crv": "P-256",
"kid": "LEu_K_tGsARSVoaGxP6nCjhYl9XB6jpuZH0vdpTEDOU",
"x": "o4lf0hDz0oej8fsiPP5UjrO8B0X-96ssgMxh1NVAVzA",
"y": "9LQcldbXC9e8KUzw6pVU-QFY2hVluMHc9-7s08iZEY4",
"alg": "ES256"
}
]
}

Convert JWK key set into BASE64 and use in environment variables

base64 jwks-es.json > output.txt
  1. SECRET_KEY : any value for hashing the password
  2. JWT_ALGO value from JWK key set alg , which is the algorithm we used : ES256.
  3. ES256_KID value from JWK key set kid
  4. ES256_KEY is the entire base64 string converted from the previous step.
# .env example
SECRET_KEY=PJQwH9Tnzx5IYrIFU_C2mEqduuWf257FkUs5NyYl8L0
JWT_ALGO=ES256
ES256_KID=5h_n5-R5z4frNsWmp9JuxIS0_hHJTaI3h1figd17PZI
# base64 jwks-es.json > output.txt
ES256_KEY=ewogICAgImtleXMiOiBbCiAgICAgICAgewogICAgICAgICAgICAia3R5IjogIkVDIiwKICAgICAgICAgICAgImQiOiAiVExqRm9lOGFhRDE4TXJqQ09ndklSQWlYcWR3RzRzdWNUVFRIS2xVTDJQWSIsCiAgICAgICAgICAgICJ1c2UiOiAic2lnIiwKICAgICAgICAgICAgImNydiI6ICJQLTI1NiIsCiAgICAgICAgICAgICJraWQiOiAiNWhfbjUtUjV6NGZyTnNXbXA5SnV4SVMwX2hISlRhSTNoMWZpZ2QxN1BaSSIsCiAgICAgICAgICAgICJ4IjogImVHSlBGeGMyQk80RWVwa0lDa1oxY2R0dVhyQ1ZnTXlPNzNfTjU5dFU0MjQiLAogICAgICAgICAgICAieSI6ICJRU1UzVFF6TjN5emdGRGtfUlZUZ1RnN0cwUTBacnRhTmhKRTBJMW1nVklnIiwKICAgICAgICAgICAgImFsZyI6ICJFUzI1NiIKICAgICAgICB9CiAgICBdCn0=

Read environment variable using python-decouple

.
├── __init__.py
├── jwks-es.json
└── output.txt
from pydantic import BaseSettings
from decouple import config
class AuthSettings(BaseSettings):
JWT_SECRET_KEY: str = config('SECRET_KEY')
ES256_KEY: str = config("ES256_KEY")
JWT_ALGO: str = config("JWT_ALGO")
ES256_KID: str = config("ES256_KID")
class Settings( AuthSettings):
pass
settings = Settings()

Create models to validate user email and password

.
├── auth.py
├── config
│ ├── __init__.py
│ ├── jwks-es.json
│ └── output.txt
├── main.py
├── models
│ ├── __init__.py
│ └── user.py
├── requirements.in
└── requirements.txt
.
├── __init__.py
└── user.py
from .user import DB_User_Model, AuthModel_User
import re
from typing import Optional
from pydantic import BaseModel, validator
from pydantic.dataclasses import dataclass
@dataclass
class DB_User_Model():
key: str
email: str
hashed_password: str
is_active: Optional[bool]
is_verified: Optional[bool]
is_superuser: Optional[bool]
class AuthModel_User(BaseModel):
email: str
password: str
@validator('password')
def passwords_validation(cls, v):
if not custom_password_validator(v):
raise ValueError("Password must have minimum 8 characters to maximum 20 characters, 1 capital letter, 1 small letter, and 1 special character from '!@#$%^&*()'")
return v
def custom_password_validator(password):
reg = "^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,20}$"
pat = re.compile(reg)
mat = re.search(pat, password)
return mat

Code for generating JWT token from JWK key set

Final step for generating JWT token

How pip-tools works, from https://pypi.org/project/pip-tools/
fastapi
uvicorn[standard]
pyjwt
cryptography
passlib[bcrypt]
python-decouple

Step 3: Validation by accessing the protected endpoints with JWT token

{
"email": "testing@gmail.com",
"password": "!Kingh_a1234"
}
"Top Secret data only authorized users can access this info"

Step 4: Build this project into container image for microservice.

FROM tiangolo/uvicorn-gunicorn:python3.9-slim
LABEL maintainer="danielchu"
ENV WORKERS_PER_CORE=4
ENV MAX_WORKERS=24
ENV LOG_LEVEL="warning"
ENV TIMEOUT="200"
RUN mkdir /auth
COPY requirements.txt /auth
WORKDIR /auth
RUN pip install -r requirements.txt
COPY . /auth
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
docker build -t auth-es256:0.0.1 .
docker run -p 8080:8000 auth-es256:0.0.1

Summary

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store