Authentication¶
Authentication lives in server/security.py and is built on AWS Cognito via fastapi-cognito.
Summary¶
auth_requiredis the main dependency for Cognito-backed API access.- Two token shapes are accepted: user tokens and M2M client-credentials tokens.
- Authentication and shop authorization are separate checks.
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.
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. |
AWS_COGNITO_MCP_CLIENT_ID |
Expected client_id for MCP server tokens (accepted alongside M2M tokens when MCP_ENABLED is true). |
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.scope inside the body.
For endpoints that require membership of the Cognito Admins group, use admin_required instead:
from server.security import admin_required
@router.get("/admin-only")
def admin_route(_ = Depends(admin_required)):
...
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¶
Authentication proves who is calling. Which shops they can touch is determined by their Cognito group membership:
- Members of the
Adminsgroup can access every shop. - All other users can only access shops whose UUID matches one of their Cognito group names. A user is given access to a shop by adding them to a Cognito group named after that shop's UUID.
GET /shops/my-shops (also exposed as the list_my_shops MCP tool) returns the list of accessible shops and a can_write flag. MCP agents are expected to call this first. The individual shop-scoped endpoints do not re-enforce this check on every request — they rely on the caller having already resolved their shop access via my-shops.
Troubleshooting¶
- 401 on every Cognito-protected route: verify
AWS_COGNITO_USERPOOL_ID, region, and client IDs in the environment. Placeholder defaults inserver/settings.pywill not work against real tokens. - 403 on an admin route: the user authenticated successfully but is not a member of the Cognito
Adminsgroup.