Authentication¶
Authentication lives in server/security.py and is built on AWS Cognito via fastapi-cognito.
Token model¶
Three credential shapes are accepted:
- User tokens — standard Cognito-issued ID/access tokens for interactive users. Validated against the configured Cognito user pool and resolved to a
client_id/ subject. - M2M (machine-to-machine) tokens — Cognito client-credentials tokens. Must carry the
/apiscope. Used by server-to-server integrations. - API keys — per-shop bearer tokens issued from
/shops/{shop_id}/api-keys/. Accepted only on routes that opt in (currently the MCP-exposed CRUD surface for products / categories / tags / attributes). See the MCP server page for issuance and usage.
The CustomCognitoToken model wraps the jose-decoded JWT and exposes the subject, scopes, and groups in a uniform shape.
- Algorithm: HS256 via
python-jose. - Password hashing (for user records we store locally): bcrypt via
passlib[bcrypt].
Configuration¶
All auth settings come from environment variables loaded by server/settings.py:
| Variable | Purpose |
|---|---|
AWS_COGNITO_USERPOOL_ID |
User pool the tokens are issued from. |
AWS_COGNITO_REGION |
AWS region of the user pool. |
AWS_COGNITO_CLIENT_ID |
Expected aud for user tokens. |
AWS_COGNITO_M2M_CLIENT_ID |
Expected client_id for M2M tokens. |
JWT_ALGORITHM |
Default HS256. |
ACCESS_TOKEN_EXPIRE_MINUTES |
Default 120. |
SESSION_SECRET |
Signing key for SessionMiddleware cookies. |
MCP_ENABLED |
Default false. Mount the MCP server at /mcp. |
Cognito itself — user pool, app clients, domain, groups — is managed outside this repo.
Dependency usage¶
Protect an endpoint with the auth_required() dependency:
from fastapi import Depends
from server.security import auth_required
@router.get("/protected")
def protected_route(token = Depends(auth_required)):
...
auth_required accepts both user and M2M tokens. For M2M-only endpoints, the handler can assert on token.scopes inside the body.
For endpoints that should also accept API keys (currently the MCP-exposed shop CRUD routes), use auth_required_any instead:
from server.security import auth_required_any
@router.get("/protected")
def protected_route(principal = Depends(auth_required_any)):
# principal is either a CustomCognitoToken or an ApiKeyTable row.
...
auth_required_any resolves X-API-Key or Authorization: Bearer sv_… first, then falls back to Cognito.
Shop access checks¶
Authentication proves who is calling; authorisation proves what shop they can touch. Shop-scoped handlers resolve the caller's UserTable row and check ShopUserTable for a link to the shop_id path parameter. M2M tokens with /api scope bypass the per-shop check (they're trusted server credentials).