Skip to content

Authentication

Authentication lives in server/security.py and is built on AWS Cognito via fastapi-cognito.

Summary

  • auth_required is 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:

  1. 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.
  2. M2M (machine-to-machine) tokens — Cognito client-credentials tokens. Must carry the /api scope. Used by server-to-server integrations.
  3. 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 Admins group 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 in server/settings.py will not work against real tokens.
  • 403 on an admin route: the user authenticated successfully but is not a member of the Cognito Admins group.