Python

Guia Completo de Autenticação em FastAPI: JWT, OAuth2 e Segurança de Endpoints

16 min de leitura

Guia Completo de Autenticação em FastAPI: JWT, OAuth2 e Segurança de Endpoints

Por que Autenticação é Crítica em APIs Modernas Quando você constrói uma API, está criando um ponto de acesso a dados ou funcionalidades que podem ser sensíveis. Sem autenticação adequada, qualquer pessoa poderia acessar informações de outros usuários, modificar dados críticos ou executar ações não autorizadas. A autenticação resolve o problema fundamental: "quem você é?". A autorização — que vem depois — responde "o que você pode fazer?". Em FastAPI, a segurança não é um add-on bolado ao final do projeto. É um pilar arquitetural que deve estar presente desde o início. O framework oferece ferramentas nativas para implementar padrões modernos como JWT (JSON Web Tokens) e OAuth2, reduzindo drasticamente o risco de vulnerabilidades comuns em APIs REST. JWT e OAuth2: Os Pilares da Autenticação Moderna O que é JWT (JSON Web Token)? Um JWT é um padrão aberto (RFC 7519) que define uma forma compacta e segura de transmitir informações entre partes. Ele é composto por três partes separadas

<h2>Por que Autenticação é Crítica em APIs Modernas</h2>

<p>Quando você constrói uma API, está criando um ponto de acesso a dados ou funcionalidades que podem ser sensíveis. Sem autenticação adequada, qualquer pessoa poderia acessar informações de outros usuários, modificar dados críticos ou executar ações não autorizadas. A autenticação resolve o problema fundamental: &quot;quem você é?&quot;. A autorização — que vem depois — responde &quot;o que você pode fazer?&quot;.</p>

<p>Em FastAPI, a segurança não é um add-on bolado ao final do projeto. É um pilar arquitetural que deve estar presente desde o início. O framework oferece ferramentas nativas para implementar padrões modernos como JWT (JSON Web Tokens) e OAuth2, reduzindo drasticamente o risco de vulnerabilidades comuns em APIs REST.</p>

<h2>JWT e OAuth2: Os Pilares da Autenticação Moderna</h2>

<h3>O que é JWT (JSON Web Token)?</h3>

<p>Um JWT é um padrão aberto (RFC 7519) que define uma forma compacta e segura de transmitir informações entre partes. Ele é composto por três partes separadas por pontos: <strong>header</strong>, <strong>payload</strong> e <strong>signature</strong>. O header define o tipo de token e o algoritmo de criptografia. O payload contém as claims (informações) que você quer transmitir, como o ID do usuário ou seu email. A signature garante que o token não foi alterado — qualquer mudança no header ou payload invalidaria a signature.</p>

<p>O grande diferencial do JWT é que ele é <strong>stateless</strong>. Diferente de sessões tradicionais, o servidor não precisa armazenar informações sobre cada token emitido. Quando o cliente envia um JWT, o servidor valida a signature e confia que as informações dentro do token são legítimas. Isso torna aplicações distribuídas muito mais escaláveis.</p>

<h3>O que é OAuth2?</h3>

<p>OAuth2 é um protocolo de <strong>autorização</strong> que define como terceiras partes podem obter acesso limitado aos recursos de um usuário sem conhecer sua senha. Ele estabelece fluxos padronizados para diferentes cenários: aplicações web, aplicações mobile, serviços backend-to-backend, etc. O fluxo mais comum em APIs é o <strong>Password Flow</strong> (ou Resource Owner Password Credentials), onde o cliente envia diretamente username e password para receber um token.</p>

<p>Em FastAPI, combinamos JWT com OAuth2: usamos OAuth2 para o fluxo de autenticação (receber credenciais e emitir um token) e JWT como o formato do token que trafega entre cliente e servidor.</p>

<h2>Implementação Prática: Autenticação Completa com JWT e OAuth2</h2>

<h3>Estrutura Base e Dependências</h3>

<p>Começamos importando as ferramentas necessárias do FastAPI e bibliotecas externas:</p>

<pre><code class="language-python">from fastapi import FastAPI, Depends, HTTPException, status

from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm

from pydantic import BaseModel, EmailStr

from passlib.context import CryptContext

from datetime import datetime, timedelta, timezone

from typing import Optional

import jwt

import os

app = FastAPI(title=&quot;API Segura com JWT&quot;)

Configurações

SECRET_KEY = os.getenv(&quot;SECRET_KEY&quot;, &quot;sua-chave-super-secreta-aqui&quot;)

ALGORITHM = &quot;HS256&quot;

ACCESS_TOKEN_EXPIRE_MINUTES = 30

Contexto para hash de senhas

pwd_context = CryptContext(schemes=[&quot;bcrypt&quot;], deprecated=&quot;auto&quot;)

Scheme OAuth2

oauth2_scheme = OAuth2PasswordBearer(tokenUrl=&quot;token&quot;)</code></pre>

<blockquote><p><strong>Importante</strong>: Nunca deixe a SECRET_KEY hardcoded em produção. Use variáveis de ambiente. A SECRET_KEY deve ser uma string longa e aleatória — quanto mais complexa, melhor.</p></blockquote>

<h3>Modelos Pydantic</h3>

<p>Define os modelos que representam usuários e tokens:</p>

<pre><code class="language-python">class Usuario(BaseModel):

id: int

username: str

email: EmailStr

full_name: Optional[str] = None

disabled: Optional[bool] = False

class UsuarioInDB(Usuario):

hashed_password: str

class Token(BaseModel):

access_token: str

token_type: str

expires_in: int

class TokenData(BaseModel):

username: Optional[str] = None</code></pre>

<h3>Funções Auxiliares</h3>

<p>Implementamos funções para hash de senhas, validação e criação de tokens:</p>

<pre><code class="language-python">def hash_password(password: str) -&gt; str:

&quot;&quot;&quot;Gera hash bcrypt da senha&quot;&quot;&quot;

return pwd_context.hash(password)

def verify_password(plain_password: str, hashed_password: str) -&gt; bool:

&quot;&quot;&quot;Verifica se a senha corresponde ao hash&quot;&quot;&quot;

return pwd_context.verify(plain_password, hashed_password)

def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -&gt; str:

&quot;&quot;&quot;Cria um JWT assinado&quot;&quot;&quot;

to_encode = data.copy()

if expires_delta:

expire = datetime.now(timezone.utc) + expires_delta

else:

expire = datetime.now(timezone.utc) + timedelta(minutes=15)

to_encode.update({&quot;exp&quot;: expire})

encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)

return encoded_jwt

def decode_token(token: str) -&gt; dict:

&quot;&quot;&quot;Decodifica e valida um JWT&quot;&quot;&quot;

try:

payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])

username: str = payload.get(&quot;sub&quot;)

if username is None:

raise HTTPException(

status_code=status.HTTP_401_UNAUTHORIZED,

detail=&quot;Token inválido&quot;

)

return payload

except jwt.ExpiredSignatureError:

raise HTTPException(

status_code=status.HTTP_401_UNAUTHORIZED,

detail=&quot;Token expirado&quot;

)

except jwt.InvalidTokenError:

raise HTTPException(

status_code=status.HTTP_401_UNAUTHORIZED,

detail=&quot;Token inválido&quot;

)</code></pre>

<h3>Banco de Dados Mock</h3>

<p>Para fins didáticos, usamos um dicionário em memória. Em produção, seria um banco de dados real:</p>

<pre><code class="language-python"># Simulando um banco de dados

fake_users_db = {

&quot;joao&quot;: {

&quot;id&quot;: 1,

&quot;username&quot;: &quot;joao&quot;,

&quot;email&quot;: &quot;joao@example.com&quot;,

&quot;full_name&quot;: &quot;João Silva&quot;,

&quot;hashed_password&quot;: hash_password(&quot;senha123&quot;),

&quot;disabled&quot;: False,

},

&quot;maria&quot;: {

&quot;id&quot;: 2,

&quot;username&quot;: &quot;maria&quot;,

&quot;email&quot;: &quot;maria@example.com&quot;,

&quot;full_name&quot;: &quot;Maria Santos&quot;,

&quot;hashed_password&quot;: hash_password(&quot;outrasenha456&quot;),

&quot;disabled&quot;: False,

}

}

def get_user(username: str) -&gt; Optional[UsuarioInDB]:

&quot;&quot;&quot;Busca usuário no &#039;banco de dados&#039;&quot;&quot;&quot;

if username in fake_users_db:

user_dict = fake_users_db[username]

return UsuarioInDB(**user_dict)

return None

def authenticate_user(username: str, password: str) -&gt; Optional[UsuarioInDB]:

&quot;&quot;&quot;Valida credenciais do usuário&quot;&quot;&quot;

user = get_user(username)

if not user:

return None

if not verify_password(password, user.hashed_password):

return None

return user</code></pre>

<h3>Dependências e Endpoints Protegidos</h3>

<p>Aqui criamos a dependência que valida o token e protege os endpoints:</p>

<pre><code class="language-python">async def get_current_user(token: str = Depends(oauth2_scheme)) -&gt; UsuarioInDB:

&quot;&quot;&quot;Dependência que valida o token e retorna o usuário atual&quot;&quot;&quot;

payload = decode_token(token)

username: str = payload.get(&quot;sub&quot;)

user = get_user(username)

if user is None:

raise HTTPException(

status_code=status.HTTP_401_UNAUTHORIZED,

detail=&quot;Usuário não encontrado&quot;

)

return user

async def get_current_active_user(

current_user: UsuarioInDB = Depends(get_current_user)

) -&gt; UsuarioInDB:

&quot;&quot;&quot;Valida se o usuário está ativo&quot;&quot;&quot;

if current_user.disabled:

raise HTTPException(

status_code=status.HTTP_400_BAD_REQUEST,

detail=&quot;Usuário desativado&quot;

)

return current_user

Endpoint para login

@app.post(&quot;/token&quot;, response_model=Token)

async def login(form_data: OAuth2PasswordRequestForm = Depends()):

&quot;&quot;&quot;Autentica o usuário e retorna um JWT&quot;&quot;&quot;

user = authenticate_user(form_data.username, form_data.password)

if not user:

raise HTTPException(

status_code=status.HTTP_401_UNAUTHORIZED,

detail=&quot;Username ou senha incorretos&quot;,

headers={&quot;WWW-Authenticate&quot;: &quot;Bearer&quot;},

)

access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)

access_token = create_access_token(

data={&quot;sub&quot;: user.username},

expires_delta=access_token_expires

)

return {

&quot;access_token&quot;: access_token,

&quot;token_type&quot;: &quot;bearer&quot;,

&quot;expires_in&quot;: ACCESS_TOKEN_EXPIRE_MINUTES * 60

}

Endpoint protegido

@app.get(&quot;/usuarios/me&quot;, response_model=Usuario)

async def read_users_me(current_user: UsuarioInDB = Depends(get_current_active_user)):

&quot;&quot;&quot;Retorna informações do usuário autenticado&quot;&quot;&quot;

return current_user

Outro endpoint protegido

@app.get(&quot;/dados-sensiveis&quot;)

async def dados_sensiveis(current_user: UsuarioInDB = Depends(get_current_active_user)):

&quot;&quot;&quot;Apenas usuários autenticados acessam&quot;&quot;&quot;

return {

&quot;message&quot;: f&quot;Olá {current_user.full_name}!&quot;,

&quot;data&quot;: &quot;Informação confidencial apenas para você&quot;

}</code></pre>

<h2>Segurança de Endpoints: Validação, Rate Limiting e Boas Práticas</h2>

<h3>Validação de Entrada e HTTPS</h3>

<p>Toda entrada de dados deve ser validada. Pydantic já faz grande parte do trabalho, mas você pode adicionar validadores customizados:</p>

<pre><code class="language-python">from pydantic import validator

class UsuarioRegistro(BaseModel):

username: str

email: EmailStr

password: str

@validator(&#039;username&#039;)

def username_alphanumeric(cls, v):

assert v.isalnum(), &#039;username deve conter apenas letras e números&#039;

assert len(v) &gt;= 3, &#039;username deve ter pelo menos 3 caracteres&#039;

return v

@validator(&#039;password&#039;)

def password_strength(cls, v):

if len(v) &lt; 8:

raise ValueError(&#039;Senha deve ter no mínimo 8 caracteres&#039;)

if not any(char.isupper() for char in v):

raise ValueError(&#039;Senha deve conter pelo menos uma letra maiúscula&#039;)

return v</code></pre>

<p>Em produção, <strong>sempre use HTTPS</strong>. O HTTP expõe tokens em texto plano. Com HTTPS, a comunicação é criptografada, protegendo JWTs em trânsito.</p>

<h3>Refresh Tokens e Expiração</h3>

<p>Tokens com expiração curta (15-30 minutos) são mais seguros. Para manter a sessão viva sem pedir as credenciais novamente, use refresh tokens:</p>

<pre><code class="language-python">def create_tokens(username: str) -&gt; dict:

&quot;&quot;&quot;Cria access_token e refresh_token&quot;&quot;&quot;

access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)

access_token = create_access_token(

data={&quot;sub&quot;: username, &quot;type&quot;: &quot;access&quot;},

expires_delta=access_token_expires

)

refresh_token_expires = timedelta(days=7)

refresh_token = create_access_token(

data={&quot;sub&quot;: username, &quot;type&quot;: &quot;refresh&quot;},

expires_delta=refresh_token_expires

)

return {

&quot;access_token&quot;: access_token,

&quot;refresh_token&quot;: refresh_token,

&quot;token_type&quot;: &quot;bearer&quot;

}

@app.post(&quot;/refresh&quot;)

async def refresh_access_token(refresh_token: str):

&quot;&quot;&quot;Emite um novo access_token usando o refresh_token&quot;&quot;&quot;

payload = decode_token(refresh_token)

if payload.get(&quot;type&quot;) != &quot;refresh&quot;:

raise HTTPException(

status_code=status.HTTP_401_UNAUTHORIZED,

detail=&quot;Token inválido&quot;

)

username = payload.get(&quot;sub&quot;)

new_access_token = create_access_token(

data={&quot;sub&quot;: username, &quot;type&quot;: &quot;access&quot;},

expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)

)

return {&quot;access_token&quot;: new_access_token, &quot;token_type&quot;: &quot;bearer&quot;}</code></pre>

<h3>Rate Limiting</h3>

<p>Proteja seus endpoints contra ataques de força bruta usando rate limiting:</p>

<pre><code class="language-python">from slowapi import Limiter

from slowapi.util import get_remote_address

limiter = Limiter(key_func=get_remote_address)

app.state.limiter = limiter

@app.post(&quot;/token&quot;)

@limiter.limit(&quot;5/minute&quot;)

async def login(request: Request, form_data: OAuth2PasswordRequestForm = Depends()):

&quot;&quot;&quot;Máximo 5 tentativas por minuto&quot;&quot;&quot;

user = authenticate_user(form_data.username, form_data.password)

if not user:

raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)

... resto do código</code></pre>

<h3>CORS (Cross-Origin Resource Sharing)</h3>

<p>Configure CORS apropriadamente para aceitar requisições apenas de origens confiáveis:</p>

<pre><code class="language-python">from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(

CORSMiddleware,

allow_origins=[&quot;https://seu-frontend.com&quot;], # Específico em produção

allow_credentials=True,

allow_methods=[&quot;GET&quot;, &quot;POST&quot;],

allow_headers=[&quot;*&quot;],

)</code></pre>

<h2>Testando a Segurança</h2>

<p>Escreva testes para garantir que sua autenticação funciona corretamente:</p>

<pre><code class="language-python">from fastapi.testclient import TestClient

client = TestClient(app)

def test_login_sucesso():

&quot;&quot;&quot;Testa login com credenciais válidas&quot;&quot;&quot;

response = client.post(

&quot;/token&quot;,

data={&quot;username&quot;: &quot;joao&quot;, &quot;password&quot;: &quot;senha123&quot;}

)

assert response.status_code == 200

assert &quot;access_token&quot; in response.json()

assert response.json()[&quot;token_type&quot;] == &quot;bearer&quot;

def test_login_falha():

&quot;&quot;&quot;Testa login com senha incorreta&quot;&quot;&quot;

response = client.post(

&quot;/token&quot;,

data={&quot;username&quot;: &quot;joao&quot;, &quot;password&quot;: &quot;senhaerrada&quot;}

)

assert response.status_code == 401

def test_endpoint_protegido_sem_token():

&quot;&quot;&quot;Testa acesso a endpoint protegido sem token&quot;&quot;&quot;

response = client.get(&quot;/usuarios/me&quot;)

assert response.status_code == 403

def test_endpoint_protegido_com_token():

&quot;&quot;&quot;Testa acesso a endpoint protegido com token válido&quot;&quot;&quot;

Primeiro faz login

login_response = client.post(

&quot;/token&quot;,

data={&quot;username&quot;: &quot;joao&quot;, &quot;password&quot;: &quot;senha123&quot;}

)

token = login_response.json()[&quot;access_token&quot;]

Acessa endpoint protegido

response = client.get(

&quot;/usuarios/me&quot;,

headers={&quot;Authorization&quot;: f&quot;Bearer {token}&quot;}

)

assert response.status_code == 200

assert response.json()[&quot;username&quot;] == &quot;joao&quot;

def test_token_expirado():

&quot;&quot;&quot;Testa acesso com token expirado&quot;&quot;&quot;

Cria um token que expira em 1 segundo

token = create_access_token(

data={&quot;sub&quot;: &quot;joao&quot;},

expires_delta=timedelta(seconds=1)

)

Aguarda a expiração

import time

time.sleep(2)

response = client.get(

&quot;/usuarios/me&quot;,

headers={&quot;Authorization&quot;: f&quot;Bearer {token}&quot;}

)

assert response.status_code == 401</code></pre>

<h2>Conclusão</h2>

<p>Dominar autenticação em FastAPI significa entender três pontos essenciais: <strong>JWT é o formato de token stateless que torna APIs escaláveis</strong>, removendo a necessidade de armazenar sessões no servidor; <strong>OAuth2 é o protocolo que define fluxos seguros para autenticação</strong>, e em FastAPI você o implementa com <code>OAuth2PasswordBearer</code> e <code>OAuth2PasswordRequestForm</code>; e <strong>segurança vai além da autenticação</strong> — inclui validação de entrada, expiração de tokens, rate limiting, HTTPS e testes. Aplicar esses conceitos desde o início do projeto evita refatorações custosas e vulnerabilidades graves.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://fastapi.tiangolo.com/tutorial/security/" target="_blank" rel="noopener noreferrer">FastAPI Security Documentation</a></li>

<li><a href="https://jwt.io/introduction" target="_blank" rel="noopener noreferrer">JWT Introduction at jwt.io</a></li>

<li><a href="https://tools.ietf.org/html/rfc6749" target="_blank" rel="noopener noreferrer">RFC 6749 - The OAuth 2.0 Authorization Framework</a></li>

<li><a href="https://owasp.org/www-project-api-security/" target="_blank" rel="noopener noreferrer">OWASP API Security Top 10</a></li>

<li><a href="https://passlib.readthedocs.io/" target="_blank" rel="noopener noreferrer">Passlib Documentation</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Python

Mocks em Python: unittest.mock, patch e pytest-mock na Prática: Do Básico ao Avançado
Mocks em Python: unittest.mock, patch e pytest-mock na Prática: Do Básico ao Avançado

Introdução: Por que Mocks são Essenciais Quando desenvolvemos software profis...

Ruff, Black e isort em Python: Linting e Formatação Automatizada: Do Básico ao Avançado
Ruff, Black e isort em Python: Linting e Formatação Automatizada: Do Básico ao Avançado

Introdução: Por que Qualidade de Código Importa Quando você começa a programa...

Boas Práticas de Estruturas de Controle em Python: if, match-case e Expressões para Times Ágeis
Boas Práticas de Estruturas de Controle em Python: if, match-case e Expressões para Times Ágeis

Introdução: O Controle de Fluxo como Base da Programação As estruturas de con...