Este documento descreve os mecanismos de autenticação e segurança implementados na StudiesAPI.
A API utiliza os seguintes mecanismos de segurança:
- JWT (JSON Web Tokens) para autenticação stateless
- Argon2 para hash de senhas
- Validação de Ownership para autorização de recursos
- HTTPS recomendado para produção
JWT (JSON Web Token) é um padrão aberto (RFC 7519) para transmissão segura de informações entre partes como um objeto JSON.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxIiwiZXhwIjoxNzEwODc2MDAwfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Partes:
- Header: Algoritmo e tipo de token
- Payload: Dados (claims)
- Signature: Assinatura para validação
| Claim | Descrição | Exemplo |
|---|---|---|
sub |
Subject (ID do usuário) | "1" |
exp |
Expiration Time | 1710876000 |
# studies_api/core/settings.py
class Settings(BaseSettings):
JWT_SECRET_KEY: str
JWT_ALGORITHM: str = 'HS256'
JWT_EXPIRATION_MINUTES: int = 30sequenceDiagram
participant C as Cliente
participant A as API
C->>A: 1. POST /auth/token (email, password)
A->>A: 2. Validar credenciais
A-->>C: 3. Retorna access_token
loop Requisições Protegidas
C->>A: 4. Request + Bearer Token
A->>A: 5. Validar token
A-->>C: 6. Response
end
Note over C,A: Token expirou
C->>A: 7. POST /auth/refresh_token
A->>A: 8. Gerar novo token
A-->>C: 9. Novo access_token
# studies_api/core/security.py
def create_access_token(data: Dict) -> str:
to_encode = data.copy()
expire = datetime.now(timezone.utc) + timedelta(
minutes=settings.JWT_EXPIRATION_MINUTES
)
to_encode.update({'exp': expire})
encoded_jwt = jwt.encode(
payload=to_encode,
key=settings.JWT_SECRET_KEY,
algorithm=settings.JWT_ALGORITHM,
)
return encoded_jwtdef verify_token(token: str) -> Dict:
try:
payload = jwt.decode(
jwt=token,
key=settings.JWT_SECRET_KEY,
algorithms=[settings.JWT_ALGORITHM],
)
return payload
except jwt.ExpiredSignatureError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail='Expired Token',
headers={'WWW-Authenticate': 'Bearer'},
)
except jwt.InvalidTokenError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail='Could Not validate credentials',
headers={'WWW-Authenticate': 'Bearer'},
)async def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security),
db: AsyncSession = Depends(get_connection),
) -> User:
payload = verify_token(token=credentials.credentials)
user_id_str = payload.get('sub')
if not user_id_str:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail='Could not validate credentials',
)
try:
user_id = int(user_id_str)
except (ValueError, TypeError):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail='Could not validate credentials',
)
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail='Could not validate credentials',
)
return user@router.get('/protected')
async def protected_route(
current_user: User = Depends(get_current_user)
):
# current_user está disponível
return {'user_id': current_user.id}A API utiliza Argon2 (vencedor do Password Hashing Competition) através da biblioteca pwdlib.
from pwdlib import PasswordHash
pwd_context = PasswordHash.recommended()def get_password_hash(password: str) -> str:
return pwd_context.hash(password)Exemplo:
plain_password = "minhasenha123"
hashed_password = get_password_hash(plain_password)
# Resultado: $argon2id$v=19$m=65536,t=3,p=4$...def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)Exemplo:
is_valid = verify_password("minhasenha123", hashed_password)
# Retorna: True ou Falseasync def authenticate_user(
email: str,
password: str,
db: AsyncSession,
) -> Optional[User]:
result = await db.execute(select(User).where(User.email == email))
user = result.scalar_one_or_none()
if not user:
return None
if not verify_password(password, user.password):
return None
return userclass UserSchema(BaseModel):
password: str
@field_validator('password')
def password_min_length(cls, v):
if len(v) < 8:
raise ValueError('Password must have more than 7 characters.')
return vCada usuário só pode acessar seus próprios recursos (sessões de estudo).
def verify_study_session_ownership(
study_session: Session,
current_user: User,
):
if study_session.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail='Do not have permissions to access this study session',
)@router.get('/sessions/{session_id}')
async def get_session(
session_id: int,
db: AsyncSession = Depends(get_connection),
current_user: User = Depends(get_current_user),
):
study_session = await db.get(Session, session_id)
if not study_session:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail='Study Session Not Found',
)
# Valida ownership
verify_study_session_ownership(
study_session=study_session,
current_user=current_user,
)
return study_session| Endpoint | Método | Segurança |
|---|---|---|
/health_check |
GET | Nenhum |
/api/v1/auth/token |
POST | Nenhum |
/api/v1/users/ |
POST | Nenhum (criação) |
| Endpoint | Método | Segurança |
|---|---|---|
/api/v1/auth/refresh_token |
POST | JWT |
/api/v1/users/ |
GET | JWT |
/api/v1/users/{id} |
GET, PUT, DELETE | JWT |
/api/v1/sessions/* |
TODOS | JWT + Ownership |
/api/v1/stats/ |
GET | JWT |
Retornado quando:
- Token ausente
- Token inválido
- Token expirado
- Credenciais incorretas
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail='Incorrect email or password',
headers={'WWW-Authenticate': 'Bearer'},
)Retornado quando:
- Usuário autenticado mas sem permissão no recurso
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail='Do not have permissions to access this study session',
)- ✅ Hash com Argon2 (algoritmo recomendado)
- ✅ Validação de tamanho mínimo (8 caracteres)
- ✅ Nunca armazenar em texto puro
- ✅ Nunca expor em logs ou respostas
- ✅ Expiração configurada (30 minutos padrão)
- ✅ Assinatura com chave secreta
- ✅ Validação de expiração automática
- ✅ Refresh token para renovação
- ✅ Validação de ownership em recursos
- ✅ Separação entre autenticação e autorização
- ✅ Princípio do menor privilégio
# Retornar header WWW-Authenticate em erros 401
headers={'WWW-Authenticate': 'Bearer'}# Configurar redirect HTTP → HTTPS
# Usar certificados SSL/TLS válidos# Gerar chave segura
python -c "import secrets; print(secrets.token_urlsafe(32))"- Desenvolvimento: 60+ minutos
- Produção: 15-30 minutos
- Refresh: Implementar blacklist de tokens
# Recomendado: Implementar rate limiting
# from slowapi import Limiter
# limiter = Limiter(key_func=get_remote_address)# Configurar origens permitidas
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["https://meudominio.com"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)# .env.production
DATABASE_URL=postgresql+asyncpg://...
JWT_SECRET_KEY=<chave_gerada_segura>
JWT_ALGORITHM=HS256
JWT_EXPIRATION_MINUTES=30| Vulnerabilidade | Como foi evitada |
|---|---|
| Senhas em texto puro | Hash com Argon2 |
| Token forging | Assinatura JWT com chave secreta |
| Token replay | Expiração de token |
| Acesso não autorizado | Validação de ownership |
| SQL Injection | SQLAlchemy ORM com parameter binding |
| Força bruta | Validação de tamanho de senha |
- Gerar JWT_SECRET_KEY forte para produção
- Configurar HTTPS no servidor
- Definir tempo de expiração adequado
- Implementar rate limiting
- Configurar CORS corretamente
- Nunca logar senhas ou tokens
- Manter dependências atualizadas
- Usar variáveis de ambiente para segredos
- Implementar blacklist de tokens (opcional)
- Adicionar headers de segurança (HSTS, CSP, etc.)
- Desenvolvimento - Guia de desenvolvimento
- Testes - Execução de testes
- Deploy - Instruções de implantação