Security Guide (Internal)
This page embeds the internal security documentation maintained under docs/Security/README.md in the repository.
Embedded reference (from Security/README.md):
Security
This guide explains how security works in Raccoon Survey across the backend (API) and frontend (UI). It covers JWT, role-based access control (RBAC), token blocklist, CORS, and the UI page guard.
Table of Contents
Overview
Security based on JWT + RBAC.
Access and refresh tokens; revocation via blocklist.
CORS with
supports_credentialsenabled.UI protects pages by checking session cookies and tokens.
The API uses
flask_jwt_extendedto issue and validate JWT:access_tokenandrefresh_token.Permissions are controlled via decorators:
@jwt_required()and@role_required(...).Tokens are revoked through an in-memory blocklist (non-persistent); migrating to DB persistence is recommended.
The frontend stores tokens in cookies and applies a guard before rendering private pages.
Security Coverage
Run tests with security-focused coverage:
pytest -q --cov=src/core --cov=src/ui --cov-report=term-missing --cov-report=html
HTML report: open
htmlcov/index.html.
Critical modules to cover (target ≥ 90%):
src/core/middlewares/rbac.py(roles and@role_required).src/core/services/auth_service.py(issue/refresh/logout/profile).src/core/services/jwt_blocklist.py(revocation byjti).src/core/__init__.py(CORS:supports_credentials, allowed origins).src/ui/routes/pages.py(private page guard).Error responses in routes:
src/core/routes/*(structure{ "message": ... }).
Per-file coverage:
pytest -q test/unitests/test_auth_service.py --cov=src/core/services/auth_service.py --cov-report=term-missing
Tips:
Simulate invalid/expired JWTs to trigger
401and403.Test routes with incorrect roles to validate RBAC.
Include CORS tests when possible (multiple origins in
.env).
Backend
JWT authentication: issuing, refresh, logout, and profile.
RBAC based on role claims.
User existence validation.
Token revocation.
CORS and allowed origins.
Swagger and Bearer scheme.
JWT Authentication
Token issuance:
Route
POST /api/v1/auth/login.Processes
emailandpasswordand validates:User exists (
@user_required(source="json", key="email", field="email")).Active role (
verify_user_active_role).Correct password (
check_password).
Generates tokens with
create_tokens(user, access_expires, refresh_expires)and claims:role,team_id,name, andidentity = user.id.
Access token refresh:
Route
POST /api/v1/auth/refreshwith@jwt_required(refresh=True).Reuses
identityand claims from the refresh token and issues a newaccess_token.
Logout (revocation):
Route
POST /api/v1/auth/logoutwith@jwt_required().Revokes the token by
jtiusingauth_service.revoke_token(jti).
Profile:
Route
GET /api/v1/auth/mewith@role_required("admin", "rrhh").Returns
{ id, role, team_id, name }extracted from the JWT.
Key code references:
src/core/services/auth_service.py(claims, issue, refresh, revoke, profile).src/core/routes/auth.py(login, refresh, logout, me).
User Validation (user_required)
The
user_requireddecorator (src/core/middlewares/user_required.py) ensures the user exists:source="jwt"(default): validates JWT and usesget_jwt_identity().source="param": looks foruser_idin the route or query.source="json": looks foruser_id(oremail) in the body.
Supports
fieldto search byid,email, or another field.Option
require_active_role=Trueenforces the role is active.
Token Blocklist
Simple in-memory implementation (
src/core/services/jwt_blocklist.py):revoke_token(jti)adds thejtito theREVOKED_TOKENSset.is_token_revoked(jti)checks if it is revoked.
Integration in
create_app(src/core/__init__.py):Callback
@jwt.token_in_blocklist_loaderusesis_token_revoked.
On app restart the state is lost. Pending implementation: DB persistence with expiration.
CORS
Configured in
create_appwithflask_cors.CORS:resources={r"/*": {"origins": BaseConfig.CORS_ORIGINS}}.supports_credentials=Trueto allow cookies/credentials.
Allowed origins via
.env:CORS_ORIGINS="https://{{base_url}},https://admin.{{base_url}}"
Swagger Security Scheme
OpenAPI available at
GET /api/v1/openapi.json.Bearerscheme (headerAuthorization: Bearer <access_token>).
Frontend
Private page guard with cookies.
Login flow and token storage.
Refresh and use of
Authorization.
Private Page Guard
Before each request, the UI blueprint validates private pages (
/dashboard,/surveys,/reports).Implementation:
src/ui/routes/pages.py(@bp.before_app_request).Access rules:
Allowed if
rs_has_session == "1"or bothrs_access_tokenandrs_refresh_tokenexist.Otherwise, redirect to
/login.
Login, Tokens and Refresh Flow
Login:
The frontend sends
POST /api/v1/auth/loginwith{ email, password }.On receiving
{ access_token, refresh_token }, it stores them in cookies:rs_access_tokenandrs_refresh_token.Optionally
rs_has_session = "1"as a session flag.
Use
HttpOnly+Securecookies (configured by the server) for greater protection.
Refresh:
When the
access_tokenexpires, sendPOST /api/v1/auth/refreshwith therefresh_token.Update
rs_access_token.
Logout:
POST /api/v1/auth/logoutand clear cookies.
Troubleshooting
403 Forbidden
Causes:
Missing
roleclaim or not allowed by@role_required("admin","rrhh").
API response (RBAC middleware):
{ "message": "forbidden" }or similar (customizable inrbac.py).
Related UI behavior:
Hides the Settings link if you are not
admin(src/ui/static/js/dashboard/nav-admin-link.js).
Quick check:
curl -i -H "Authorization: Bearer <token-no-admin>" http://{{base_url}}/api/v1/metrics/dashboard
404 Not Found
Common causes:
Non-existent entity:
role not found,team not found,category not found.Invalid anonymous token: does not exist or already used.
API responses (real examples):
Roles:
{ "message": "role not found" }(src/core/routes/roles.py).Teams:
{ "message": "team not found" }(src/core/routes/teams.py).Categories:
{ "message": "category not found" }(src/core/routes/categories.py).Anonymous:
{ "message": "token not found" }or similar (src/core/routes/anonymous.py).
Related UI behavior:
The UI typically shows
alert(...)orconsole.error(...)and retries/reloads (seesrc/ui/static/js/config/categories.js,users.js).
500 Internal Server Error
Common causes:
Database errors, unchecked validations, service exceptions.
API responses (example):
Anonymous:
except RuntimeError as e → return jsonify({ "message": str(e) }), 500(src/core/routes/anonymous.py).
Diagnostic steps:
Review server logs and stack trace.
Validate payloads with
Content-Type: application/jsonand expected structure.
Useful Snippets
Login:
curl -s -X POST http://{{base_url}}/api/v1/auth/login -H 'Content-Type: application/json' -d '{"email":"admin@raccoon.local","password":"<pass>"}'
Refresh:
curl -s -X POST http://{{base_url}}/api/v1/auth/refresh -H 'Authorization: Bearer <refresh_token>'
Access a protected endpoint:
curl -i -H 'Authorization: Bearer <access_token>' http://{{base_url}}/api/v1/metrics/dashboard