Skip to content

HTTP Basic Auth

For simple scenarios, HTTP Basic Auth can be used.

With HTTP Basic Auth, the application expects a header containing a username and password.

If the header is missing, the application responds with an HTTP 401 "Unauthorized" error and includes a WWW-Authenticate header with a value of Basic and an optional realm parameter.

This prompts the browser to display a login dialog for the username and password. Once entered, the browser automatically sends the credentials in the header.

Simple HTTP Basic Auth

  1. Import HTTPBasic and HTTPBasicCredentials.
  2. Create a security scheme using HTTPBasic.
  3. Apply this security scheme as a dependency in your path operation.
  4. The dependency returns an HTTPBasicCredentials object, which includes the provided username and password.
from typing import Dict

from esmerald import (
    Esmerald,
    Gateway,
    Inject,
    Injects,
    get,
)
from esmerald.security.http import HTTPBasic, HTTPBasicCredentials

security = HTTPBasic()


@get("/users/me", dependencies={"credentials": Inject(security)}, security=[security])
def get_current_user(credentials: HTTPBasicCredentials = Injects()) -> Dict[str, str]:
    return {"username": credentials.username, "password": credentials.password}


app = Esmerald(
    routes=[
        Gateway(handler=get_current_user),
    ]
)

When you first open the URL (or click the "Execute" button in the docs), the browser will prompt you for your username and password:

Basic

Verify the Username

Here's a more comprehensive example.

Use a dependency to verify if the username and password are correct.

For this, use the Python standard module secrets to check the username and password.

secrets.compare_digest() requires bytes or a str containing only ASCII characters, meaning it won't work with characters like ú, as in Araújo.

To handle this, first convert the username and password to bytes by encoding them with UTF-8.

Then use secrets.compare_digest() to ensure that credentials.username is "alice123" and credentials.password is "sunshine".

import secrets
from typing import Dict


from esmerald import Esmerald, Gateway, HTTPException, Inject, Injects, Security, get, status
from esmerald.param_functions import Security
from esmerald.security.http import HTTPBasic, HTTPBasicCredentials

security = HTTPBasic()


def get_username(credentials: HTTPBasicCredentials = Security(security)):
    correct_username = "alice123"
    correct_password = "sunshine"

    if not (
        secrets.compare_digest(credentials.username, correct_username)
        and secrets.compare_digest(credentials.password, correct_password)
    ):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Basic"},
        )
    return credentials.username


@get("/users/me", dependencies={"username": Inject(get_username)}, security=[security])
def get_current_user(username: str = Injects()) -> Dict[str, str]:
    return {"username": username}


app = Esmerald(
    routes=[
        Gateway(handler=get_current_user),
    ],
)

This would be similar to:

if not (credentials.username == "alice123") or not (credentials.password == "sunshine"):
    # Return some error
    ...

Timing Attacks

What exactly is a "timing attack"?

Imagine some attackers are attempting to figure out a valid username and password combination.

They send a request with the username alice123 and the password sunshine.

In your Python application, the logic might look something like this:

if "alice123" == "charlie_admin" and "sunshine" == "openSesame":
    ...

When Python compares the first character of alice123 (a) with the first character of charlie_admin (c), it instantly determines that the strings do not match and returns False. No further comparisons are needed because the mismatch is already clear. Consequently, your application responds with "Invalid username or password."

Next, the attackers try a different username, such as charlie_adminx, with the same password sunshine.

Your application logic then processes something like this:

if "charlie_adminx" == "charlie_admin" and "sunshine" == "openSesame":
    ...

Python will need to compare the entire string charlie_admi in both charlie_adminx and charlie_admin before determining they are not the same. This will take a few extra microseconds to respond with "Invalid username or password."

Here’s the rewritten version with new names and terms:

The time to respond helps attackers

Attackers can notice that the server took slightly longer to respond with "Invalid username or password." This indicates that some initial characters in the username might be correct.

They can then try again, refining their guesses, knowing that the correct username is likely closer to charlie_adminx than alice123.

Automated Attacks

Attackers typically don't guess usernames and passwords manually. Instead, they use scripts to automate the process, making thousands or even millions of attempts per second. These scripts can identify one correct character at a time.

By exploiting timing information unintentionally leaked by the application, attackers can eventually determine the correct username and password within minutes or hours.

Fix it with secrets.compare_digest()

Using secrets.compare_digest() in our code ensures that comparing any two strings, such as charlie_adminx to charlie_admin or alice123 to charlie_admin, takes the same amount of time. This also applies to password comparisons.

By integrating secrets.compare_digest() into your application, you can effectively protect against timing attacks.

Return the error

If the credentials are incorrect, return an HTTPException with a status code of 401. This is the same status code used when no credentials are provided. Additionally, include the WWW-Authenticate header to prompt the browser to display the login screen again:

import secrets
from typing import Dict


from esmerald import Esmerald, Gateway, HTTPException, Inject, Injects, Security, get, status
from esmerald.param_functions import Security
from esmerald.security.http import HTTPBasic, HTTPBasicCredentials

security = HTTPBasic()


def get_username(credentials: HTTPBasicCredentials = Security(security)):
    correct_username = "alice123"
    correct_password = "sunshine"

    if not (
        secrets.compare_digest(credentials.username, correct_username)
        and secrets.compare_digest(credentials.password, correct_password)
    ):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Basic"},
        )
    return credentials.username


@get("/users/me", dependencies={"username": Inject(get_username)}, security=[security])
def get_current_user(username: str = Injects()) -> Dict[str, str]:
    return {"username": username}


app = Esmerald(
    routes=[
        Gateway(handler=get_current_user),
    ],
)

Notes

These step by step guides were inspired by FastAPI great work of providing simple and yet effective examples for everyone to understand.

Esmerald adopts a different implementation internally but with the same purposes as any other framework to achieve that.