Skip to main content
Definable uses JSON Web Tokens (JWT) as the primary authentication mechanism, providing a stateless, secure way to verify user identity across API requests.
Definable uses Stytch as its authentication provider. JWT tokens are issued by Stytch and validated locally using JWKS (JSON Web Key Set). For detailed information about the Stytch integration, see the Stytch Integration guide.

What are JWTs?

JSON Web Tokens are an open standard (RFC 7519) for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. In Definable, JWTs are used to:
  • Authenticate users after login
  • Maintain session state without server-side storage
  • Pass user identity information securely between services

JWT Structure

JWTs consist of three parts separated by dots:
header.payload.signature
Contains the token type and signing algorithm:
{
  "alg": "RS256",
  "typ": "JWT"
}
Definable uses RS256 (asymmetric encryption) via Stytch, not HS256. RS256 uses public/private key pairs for enhanced security.

Payload

Contains the claims or user data:
{
  "sub": "user-live-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "aud": "project-live-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "iss": "stytch.com/project-live-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "exp": 1619283492,  // Expiration timestamp
  "iat": 1619197092   // Issued at timestamp
}
Key Claims:
  • sub (subject): Stytch user ID
  • aud (audience): Stytch project ID
  • iss (issuer): Stytch project identifier
  • exp (expiration): Token expiration timestamp
  • iat (issued at): Token creation timestamp

Signature

Ensures the token hasn’t been tampered with using RS256 asymmetric encryption:
RSASHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  stytch_private_key
)
The signature is verified using Stytch’s public key obtained from their JWKS endpoint, which is cached locally for performance.

Implementation in Definable

Token Generation

When a user logs in successfully via Stytch, the system receives a session JWT from Stytch:
# User authentication via Stytch
async def post_test_login(self, test_login: TestLogin) -> TestResponse:
    authenticate_user_response = await stytch_base.authenticate_user_with_password(
        test_login.email,
        test_login.password
    )

    if authenticate_user_response.success:
        # Return Stytch session JWT
        return TestResponse(token=authenticate_user_response.data.session_jwt)
The Stytch JWT contains:
  • Stytch user ID (sub)
  • Stytch project ID (aud)
  • Issuer information (iss)
  • Expiration time (exp) - default 24 hours
  • Issued at time (iat)
  • No sensitive information like passwords
Definable also generates internal JWT tokens for API keys using create_access_token() with HS256, but user session tokens come from Stytch and use RS256.

Token Validation

Every protected endpoint uses the JWTBearer dependency for validation via Stytch JWKS:
class JWTBearer(HTTPBearer):
    async def __call__(
        self,
        request: Request = None,
        websocket: WebSocket = None,
        session: AsyncSession = Depends(get_db),
    ) -> Any:
        if request:
            credentials = await super().__call__(request)
            if not credentials or credentials.scheme != "Bearer":
                raise HTTPException(status_code=403, detail="Invalid authorization")

            # Verify JWT using Stytch JWKS
            response = await stytch_base.authenticate_user_with_jkws(credentials.credentials)
            if response.success:
                # Look up user by stytch_id from token's "sub" claim
                user_query = select(UserModel).where(UserModel.stytch_id == response.data["sub"])
                user_result = await session.execute(user_query)
                user = user_result.scalar_one_or_none()

                if not user:
                    raise HTTPException(status_code=403, detail="User not found")

                return {"stytch_user_id": response.data["sub"], "id": str(user.id)}
            else:
                raise HTTPException(status_code=403, detail="Access denied")
This middleware:
  1. Extracts the token from the Authorization header
  2. Fetches Stytch’s public keys from JWKS endpoint (cached)
  3. Verifies the token signature using RS256 and Stytch’s public key
  4. Validates token claims (exp, iat, aud, sub, iss)
  5. Looks up the internal user using the stytch_id from the token
  6. Returns both the Stytch user ID and internal user ID
Key Differences from Generic JWT:
  • Uses JWKS for public key retrieval (no shared secret)
  • Validates against Stytch project ID (audience claim)
  • Requires database lookup to map Stytch ID to internal user ID
  • Supports both HTTP requests and WebSocket connections

Using JWT in Requests

Client applications must include the JWT in the Authorization header:
GET /api/some-endpoint HTTP/1.1
Host: api.definable.ai
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVmOGU3ZDZjLTkyYTQtNGMzYi1iM2UyLWYxYTdiOGQ5YzBlMSIsImV4cCI6MTYxOTI4MzQ5Mn0.8Kq1MseugX_dnLAT4ZPwTN7wxpY7zLBVyWiYw2ki8a0

JWT and RBAC Integration

JWTs work in conjunction with the Role-Based Access Control (RBAC) system:
  1. The JWT establishes user identity
  2. The RBAC middleware uses this identity to determine:
    • The user’s role in the requested organization
    • Permissions associated with that role
    • Whether the user can perform the requested action
@app.get("/api/protected")
async def protected_route(
    user: dict = Depends(RBAC("resource", "action"))
):
    # If execution reaches here, the user is authenticated
    # and authorized to access this endpoint
    return {"message": "You have access", "user_id": user["id"]}

JWT Configuration

Definable’s Stytch JWT implementation requires these environment variables:
# Stytch Configuration
STYTCH_PROJECT_ID=project-test-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
STYTCH_SECRET=secret-test-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
STYTCH_ENVIRONMENT=test  # or "live" for production

# Internal JWT for API Keys (separate from Stytch session tokens)
JWT_SECRET=your-secure-secret-key
JWT_EXPIRE_MINUTES=60
User session tokens come from Stytch and are validated via JWKS. The JWT_SECRET is only used for internal API keys, not for user authentication.

WebSocket Authentication

For WebSocket connections, JWTs are passed as query parameters:
const socket = new WebSocket('wss://api.definable.ai/ws?token=your-jwt-token');
The server validates these tokens using the same mechanism:
async def __call__(self, websocket: WebSocket = None):
    token = websocket.query_params.get("token")
    if not token:
        raise HTTPException(status_code=403, detail="Invalid authorization")
    try:
        payload = jwt.decode(token, settings.jwt_secret, algorithms=["HS256"])
        return payload
    except jwt.InvalidTokenError:
        raise HTTPException(status_code=403, detail="Invalid or expired token")

Security Considerations

Definable’s JWT implementation with Stytch follows these security best practices:
  • Asymmetric Encryption (RS256): More secure than symmetric HS256 - private key never leaves Stytch
  • JWKS Key Rotation: Stytch can rotate keys without requiring code changes
  • Public Key Caching: Keys cached for 10 minutes to balance security and performance
  • Token Expiration: Stytch session tokens expire after 24 hours by default (configurable)
  • No Sensitive Data: Tokens contain only user IDs and metadata, never passwords
  • HTTPS Only: JWTs are only transmitted over encrypted connections
  • Claim Validation: All required claims (exp, iat, aud, sub, iss) are verified
  • Audience Validation: Ensures token was issued for your Stytch project

Troubleshooting

This usually means:
  • The token has expired
  • The token was signed with a different secret
  • The token has been tampered with
Solution: Re-authenticate by logging in again.
Check that:
  • The Authorization header is included
  • The header uses the format Bearer <token>
  • There are no extra spaces or characters
JWTs have a limited lifetime. When expired:
  • The client needs to request a new token
  • If using refresh tokens, use the refresh flow to get a new access token

Next Steps