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
eHTTPBasicCredentials
. - Crie um "esquema
security
" utilizandoHTTPBasic
. - 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 opassword
enviado.
- Isto contém o
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}