Ir para o conteúdo

HTTP Basic Auth

Para os casos mais simples, você pode utilizar o HTTP Basic Auth.

No HTTP Basic Auth, a aplicação espera um cabeçalho que contém um usuário e uma senha.

Caso ela não receba, ela retorna um erro HTTP 401 "Unauthorized" (Não Autorizado).

E retorna um cabeçalho WWW-Authenticate com o valor Basic, e um parâmetro opcional realm.

Isso sinaliza ao navegador para mostrar o prompt integrado para um usuário e senha.

Então, quando você digitar o usuário e senha, o navegador os envia automaticamente no cabeçalho.

HTTP Basic Auth Simples

  • Importe HTTPBasic e HTTPBasicCredentials.
  • Crie um "esquema security" utilizando HTTPBasic.
  • Utilize o security com uma dependência em sua operação de rota.
  • Isso retorna um objeto do tipo HTTPBasicCredentials:
    • Isto contém o username e o password enviado.
from typing import Annotated

from fastapi import Depends, FastAPI
from fastapi.security import HTTPBasic, HTTPBasicCredentials

app = FastAPI()

security = HTTPBasic()


@app.get("/users/me")
def read_current_user(credentials: Annotated[HTTPBasicCredentials, Depends(security)]):
    return {"username": credentials.username, "password": credentials.password}
🤓 Other versions and variants
from fastapi import Depends, FastAPI
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from typing_extensions import Annotated

app = FastAPI()

security = HTTPBasic()


@app.get("/users/me")
def read_current_user(credentials: Annotated[HTTPBasicCredentials, Depends(security)]):
    return {"username": credentials.username, "password": credentials.password}

Tip

Prefer to use the Annotated version if possible.

from fastapi import Depends, FastAPI
from fastapi.security import HTTPBasic, HTTPBasicCredentials

app = FastAPI()

security = HTTPBasic()


@app.get("/users/me")
def read_current_user(credentials: HTTPBasicCredentials = Depends(security)):
    return {"username": credentials.username, "password": credentials.password}

Quando você tentar abrir a URL pela primeira vez (ou clicar no botão "Executar" nos documentos) o navegador vai pedir pelo seu usuário e senha:

Verifique o usuário

Aqui está um exemplo mais completo.

Utilize uma dependência para verificar se o usuário e a senha estão corretos.

Para isso, utilize o módulo padrão do Python secrets para verificar o usuário e senha.

O secrets.compare_digest() necessita receber bytes ou str que possuem apenas caracteres ASCII (os em inglês). Isso significa que não funcionaria com caracteres como o á, como em Sebastián.

Para lidar com isso, primeiramente nós convertemos o username e o password para bytes, codificando-os com UTF-8.

Então nós podemos utilizar o secrets.compare_digest() para garantir que o credentials.username é "stanleyjobson", e que o credentials.password é "swordfish".

import secrets
from typing import Annotated

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials

app = FastAPI()

security = HTTPBasic()


def get_current_username(
    credentials: Annotated[HTTPBasicCredentials, Depends(security)],
):
    current_username_bytes = credentials.username.encode("utf8")
    correct_username_bytes = b"stanleyjobson"
    is_correct_username = secrets.compare_digest(
        current_username_bytes, correct_username_bytes
    )
    current_password_bytes = credentials.password.encode("utf8")
    correct_password_bytes = b"swordfish"
    is_correct_password = secrets.compare_digest(
        current_password_bytes, correct_password_bytes
    )
    if not (is_correct_username and is_correct_password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Basic"},
        )
    return credentials.username


@app.get("/users/me")
def read_current_user(username: Annotated[str, Depends(get_current_username)]):
    return {"username": username}
🤓 Other versions and variants
import secrets

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from typing_extensions import Annotated

app = FastAPI()

security = HTTPBasic()


def get_current_username(
    credentials: Annotated[HTTPBasicCredentials, Depends(security)],
):
    current_username_bytes = credentials.username.encode("utf8")
    correct_username_bytes = b"stanleyjobson"
    is_correct_username = secrets.compare_digest(
        current_username_bytes, correct_username_bytes
    )
    current_password_bytes = credentials.password.encode("utf8")
    correct_password_bytes = b"swordfish"
    is_correct_password = secrets.compare_digest(
        current_password_bytes, correct_password_bytes
    )
    if not (is_correct_username and is_correct_password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Basic"},
        )
    return credentials.username


@app.get("/users/me")
def read_current_user(username: Annotated[str, Depends(get_current_username)]):
    return {"username": username}

Tip

Prefer to use the Annotated version if possible.

import secrets

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials

app = FastAPI()

security = HTTPBasic()


def get_current_username(credentials: HTTPBasicCredentials = Depends(security)):
    current_username_bytes = credentials.username.encode("utf8")
    correct_username_bytes = b"stanleyjobson"
    is_correct_username = secrets.compare_digest(
        current_username_bytes, correct_username_bytes
    )
    current_password_bytes = credentials.password.encode("utf8")
    correct_password_bytes = b"swordfish"
    is_correct_password = secrets.compare_digest(
        current_password_bytes, correct_password_bytes
    )
    if not (is_correct_username and is_correct_password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Basic"},
        )
    return credentials.username


@app.get("/users/me")
def read_current_user(username: str = Depends(get_current_username)):
    return {"username": username}

Isso seria parecido com:

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

Porém, ao utilizar o secrets.compare_digest(), isso estará seguro contra um tipo de ataque chamado "timing attacks" (ataques de temporização).

Ataques de Temporização

Mas o que é um "timing attack" (ataque de temporização)?

Vamos imaginar que alguns invasores estão tentando adivinhar o usuário e a senha.

E eles enviam uma requisição com um usuário johndoe e uma senha love123.

Então o código Python em sua aplicação seria equivalente a algo como:

if "johndoe" == "stanleyjobson" and "love123" == "swordfish":
    ...

Mas no exato momento que o Python compara o primeiro j em johndoe contra o primeiro s em stanleyjobson, ele retornará False, porque ele já sabe que aquelas duas strings não são a mesma, pensando que "não existe a necessidade de desperdiçar mais poder computacional comparando o resto das letras". E a sua aplicação dirá "Usuário ou senha incorretos".

Então os invasores vão tentar com o usuário stanleyjobsox e a senha love123.

E a sua aplicação faz algo como:

if "stanleyjobsox" == "stanleyjobson" and "love123" == "swordfish":
    ...

O Python terá que comparar todo o stanleyjobso tanto em stanleyjobsox como em stanleyjobson antes de perceber que as strings não são a mesma. Então isso levará alguns microssegundos a mais para retornar "Usuário ou senha incorretos".

O tempo para responder ajuda os invasores

Neste ponto, ao perceber que o servidor demorou alguns microssegundos a mais para enviar o retorno "Usuário ou senha incorretos", os invasores irão saber que eles acertaram alguma coisa, algumas das letras iniciais estavam certas.

E eles podem tentar de novo sabendo que provavelmente é algo mais parecido com stanleyjobsox do que com johndoe.

Um ataque "profissional"

Claro, os invasores não tentariam tudo isso de forma manual, eles escreveriam um programa para fazer isso, possivelmente com milhares ou milhões de testes por segundo. E obteriam apenas uma letra a mais por vez.

Mas fazendo isso, em alguns minutos ou horas os invasores teriam adivinhado o usuário e senha corretos, com a "ajuda" da nossa aplicação, apenas usando o tempo levado para responder.

Corrija com o secrets.compare_digest()

Mas em nosso código já estamos utilizando o secrets.compare_digest().

Resumindo, levará o mesmo tempo para comparar stanleyjobsox com stanleyjobson do que comparar johndoe com stanleyjobson. E o mesmo para a senha.

Deste modo, ao utilizar secrets.compare_digest() no código de sua aplicação, ela estará a salvo contra toda essa gama de ataques de segurança.

Retorne o erro

Após detectar que as credenciais estão incorretas, retorne um HTTPException com o status 401 (o mesmo retornado quando nenhuma credencial foi informada) e adicione o cabeçalho WWW-Authenticate para fazer com que o navegador mostre o prompt de login novamente:

import secrets
from typing import Annotated

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials

app = FastAPI()

security = HTTPBasic()


def get_current_username(
    credentials: Annotated[HTTPBasicCredentials, Depends(security)],
):
    current_username_bytes = credentials.username.encode("utf8")
    correct_username_bytes = b"stanleyjobson"
    is_correct_username = secrets.compare_digest(
        current_username_bytes, correct_username_bytes
    )
    current_password_bytes = credentials.password.encode("utf8")
    correct_password_bytes = b"swordfish"
    is_correct_password = secrets.compare_digest(
        current_password_bytes, correct_password_bytes
    )
    if not (is_correct_username and is_correct_password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Basic"},
        )
    return credentials.username


@app.get("/users/me")
def read_current_user(username: Annotated[str, Depends(get_current_username)]):
    return {"username": username}
🤓 Other versions and variants
import secrets

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from typing_extensions import Annotated

app = FastAPI()

security = HTTPBasic()


def get_current_username(
    credentials: Annotated[HTTPBasicCredentials, Depends(security)],
):
    current_username_bytes = credentials.username.encode("utf8")
    correct_username_bytes = b"stanleyjobson"
    is_correct_username = secrets.compare_digest(
        current_username_bytes, correct_username_bytes
    )
    current_password_bytes = credentials.password.encode("utf8")
    correct_password_bytes = b"swordfish"
    is_correct_password = secrets.compare_digest(
        current_password_bytes, correct_password_bytes
    )
    if not (is_correct_username and is_correct_password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Basic"},
        )
    return credentials.username


@app.get("/users/me")
def read_current_user(username: Annotated[str, Depends(get_current_username)]):
    return {"username": username}

Tip

Prefer to use the Annotated version if possible.

import secrets

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials

app = FastAPI()

security = HTTPBasic()


def get_current_username(credentials: HTTPBasicCredentials = Depends(security)):
    current_username_bytes = credentials.username.encode("utf8")
    correct_username_bytes = b"stanleyjobson"
    is_correct_username = secrets.compare_digest(
        current_username_bytes, correct_username_bytes
    )
    current_password_bytes = credentials.password.encode("utf8")
    correct_password_bytes = b"swordfish"
    is_correct_password = secrets.compare_digest(
        current_password_bytes, correct_password_bytes
    )
    if not (is_correct_username and is_correct_password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Basic"},
        )
    return credentials.username


@app.get("/users/me")
def read_current_user(username: str = Depends(get_current_username)):
    return {"username": username}