Skip to main content

Principal types

DynamoSQL recognizes two principal types:
TypeDescriptionHow to authenticate
Portal userHuman user who logged in via the DynamoSQL portalPasskey or password + TOTP; portal issues a session
API clientMachine-to-machine service account created in the portalOAuth 2.0 client credentials grant
The /v1/query endpoint is intended for API clients. Portal users typically interact through the portal’s SQL editor.

OAuth 2.0 client credentials

API clients authenticate with the Cognito token endpoint using the client credentials grant:
POST https://auth.dynamosql.com/oauth2/token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
&scope=query:execute
The response contains an access_token (JWT) and expires_in (seconds). Tokens are valid for one hour.

Required scopes

ScopeRequired for
query:executePOST /v1/query with mode: "execute"
query:planPOST /v1/query with mode: "plan"
Request only the scopes you need. If your client only needs plan inspection (e.g., a CI/CD pipeline that validates queries without running them), request query:plan only.

Passing the bearer token

Include the token in the Authorization header on every request:
Authorization: Bearer YOUR_ACCESS_TOKEN

Token refresh

Tokens cannot be refreshed with the client credentials grant. When a token expires, simply request a new one using the same credentials exchange. Cache the token and re-request it when you receive a 401 response.
import requests
import time

class TokenCache:
    def __init__(self, client_id, client_secret):
        self.client_id = client_id
        self.client_secret = client_secret
        self._token = None
        self._expires_at = 0

    def get(self):
        if time.time() < self._expires_at - 30:  # 30s buffer
            return self._token
        resp = requests.post(
            "https://auth.dynamosql.com/oauth2/token",
            data={
                "grant_type": "client_credentials",
                "client_id": self.client_id,
                "client_secret": self.client_secret,
                "scope": "query:execute",
            },
        )
        resp.raise_for_status()
        body = resp.json()
        self._token = body["access_token"]
        self._expires_at = time.time() + body["expires_in"]
        return self._token

Tenant scoping

Every JWT issued by Cognito carries a tenantId claim. The Lambda handler reads this claim and scopes all DynamoDB access to that tenant’s tables. You cannot query another tenant’s data. The tenantId field in the request body is optional. If provided, it must match the tenantId in the JWT — a mismatch returns 403.

Non-production environment

Use auth.dev.dynamosql.com and api.dev.dynamosql.com for testing. Credentials are separate per environment.