Authenticate a user and receive access and refresh tokens. Supports both global login (for developers and operators) and project-scoped login (for end users).
Request
Body
User’s password (minimum 8 characters)
Required for end users only. Project UUID for project-scoped authentication. Developers and operators do not provide this header.
Response
Success Response (200 OK)
JWT access token for API authentication. Expires in 30 minutes (configurable via ACCESS_TOKEN_EXPIRE_MINUTES). Contains claims: sub (user_id), type (“access”), exp (expiration), and project_id (for end users only).
Refresh token to obtain new access tokens. Expires in 7 days. Contains claims: sub (user_id), type (“refresh”), exp (expiration), and project_id (for end users only).
Token type (always “bearer”)
Example Requests
End User Login (Project-Scoped)
End users must provide X-Project-ID header to authenticate within their specific project context:
curl -X POST https://api.vibecoding.ad/api/v1/auth/login \
-H "Content-Type: application/json" \
-H "X-Project-ID: 550e8400-e29b-41d4-a716-446655440000" \
-d '{
"email": "user@example.com",
"password": "SecurePass123"
}'
Developer/Operator Login (Global)
Developers and operators authenticate globally without project context:
curl -X POST https://api.vibecoding.ad/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "developer@example.com",
"password": "SecurePass123"
}'
Response
{
"access_token" : "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." ,
"refresh_token" : "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." ,
"token_type" : "bearer"
}
Backend Implementation Flow
Header Validation : Extract optional X-Project-ID header and validate UUID format
Command Creation : LoginUserCommand created with email, password, and optional project_id
User Lookup :
If project_id provided: UserService.get_user_by_email_and_project() queries for end user
If no project_id: UserService.get_user_id_by_email() queries for operator/developer
Role Validation : For project-scoped login, validates user role is END_USER
Aggregate Load : LoginUserHandler loads user aggregate from event store via repository.get_by_id_or_raise()
Password Verification : UserActions.login() validates:
Hashed password exists
Password matches via pwd_context.verify() (bcrypt)
Raises ValueError("Invalid password") if verification fails
Event Emission : UserWasLoggedIn event raised with user_id and email
Event Persistence : Event saved to event store and published to PubSub
Token Generation :
Access token: 30 minutes expiry, contains sub, type: "access", exp, and project_id (for end users)
Refresh token: 7 days expiry, contains sub, type: "refresh", exp, and project_id (for end users)
Algorithm: HS256
Secret: settings.SECRET_KEY
Response : Returns TokenResponse with both tokens and type “bearer”
Token Usage
Access Token
Use the access token in the Authorization header for subsequent API requests:
curl https://api.vibecoding.ad/api/v1/auth/me \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
Properties:
Expiry : 30 minutes (configurable via settings.ACCESS_TOKEN_EXPIRE_MINUTES)
Algorithm : HS256
Claims :
sub: User ID (UUID string)
type: “access”
exp: Expiration timestamp
project_id: Project UUID (included only for end users)
Purpose : Authenticate API requests
Validation : Via get_current_user() dependency using jwt.decode()
Refresh Token
Use the refresh token to obtain a new access token via /api/v1/auth/refresh:
curl -X POST https://api.vibecoding.ad/api/v1/auth/refresh \
-H "Content-Type: application/json" \
-d '{"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}'
Properties:
Expiry : 7 days
Algorithm : HS256
Claims :
sub: User ID (UUID string)
type: “refresh”
exp: Expiration timestamp
project_id: Project UUID (included only for end users)
Storage : httpOnly cookies recommended
Security : Single-use pattern recommended
Project-Scoped Authentication
End users are authenticated within the context of a specific project using the X-Project-ID header:
Authentication Flow
Registration : End user registered with X-Project-ID header, user record includes project_id field in database
Login Request : Must provide same X-Project-ID header matching registered project
User Lookup : Backend queries users table filtering by email AND project_id:
UserService.get_user_by_email_and_project(email, project_id)
Role Validation : Verifies user role is END_USER (raises ValueError if not)
JWT Token : Generated with project_id claim for authorization
API Access : All subsequent requests scoped to user’s project via JWT project_id claim
Email Uniqueness Model
The same email can exist as multiple user types due to project scoping:
User Type Project ID Namespace End user in Project A UUID Project A End user in Project B UUID Project B Developer NULL Global Operator NULL Global
Database Implementation:
Unique constraint: email + project_id (allows same email across different projects)
Partial unique index: Allows NULL project_id for operators/developers with same email as end users
Query logic uses X-Project-ID header to determine which account to authenticate
Project ID Validation
Format Requirements:
Must be valid UUID format
Validated via uuid.UUID(project_id_header) in endpoint
Returns 400 Bad Request with message “Invalid X-Project-ID format. Must be a valid UUID.” if invalid
Missing Project ID:
End users attempting global login (without X-Project-ID) will fail with “Invalid email or password”
System performs user lookup that requires project_id match, preventing cross-project access
Error Responses
Invalid Credentials (401 Unauthorized)
Returned when email or password is incorrect, or user not found:
{
"detail" : "Incorrect email or password"
}
Triggered by:
ValueError("Invalid email or password") from LoginUserHandler
ValueError("Invalid password") from UserActions.login() when bcrypt verification fails
User not found in database lookup
Headers:
{
"WWW-Authenticate" : "Bearer"
}
Account Not Activated (400 Bad Request)
Returned when user exists but hasn’t verified their email:
{
"detail" : "Account not activated"
}
Triggered by:
ValueError containing “not active” from aggregate validation
User is_active field is false
Returned when X-Project-ID header is provided but not a valid UUID:
{
"detail" : "Invalid X-Project-ID format. Must be a valid UUID."
}
Triggered by:
uuid.UUID(project_id_header) raises ValueError in endpoint
Example invalid values: “not-a-uuid”, “123”, ""
Internal Server Error (500)
Returned for unexpected errors during login:
{
"detail" : "Login failed: <error_message>"
}
Common Causes:
Database connection failures
Event store persistence errors
JWT encoding failures
Security Best Practices
Always store tokens securely using httpOnly cookies. Never store tokens in localStorage or sessionStorage to prevent XSS attacks.
Implementation Recommendations:
Token Storage : Use httpOnly cookies with secure flag in production
Frontend: storeTokensInCookies() helper sets cookies with httpOnly: true, secure: <protocol-based>, sameSite: 'lax'
Cookie names: devkit4ai-token (access), devkit4ai-refresh-token (refresh)
Expiry: Matches token expiry (30 min for access, 7 days for refresh)
HTTPS Only : Always use HTTPS in production to protect tokens in transit
Secure flag automatically enabled when protocol is HTTPS
Local development (localhost) excluded from secure requirement
Token Refresh : Implement automatic token refresh before expiry
Access token expires in 30 minutes
Refresh endpoint: POST /api/v1/auth/refresh
Client should refresh ~5 minutes before expiry
Logout : Clear both access and refresh tokens on logout
Delete cookies: clearTokensFromCookies() helper
Backend doesn’t maintain session state (stateless JWT)
Rate Limiting : Implement rate limiting to prevent brute force attacks
Recommended: 5 attempts per 15 minutes per IP
Consider CAPTCHA after 3 failed attempts
Password Security :
Backend uses bcrypt for password hashing via pwd_context.verify()
Minimum 8 characters enforced during registration
Recommend requiring uppercase, lowercase, and digit
Error Messages : Generic “Incorrect email or password” to prevent user enumeration
Same message for invalid email, invalid password, or inactive account in some cases
Don’t reveal whether email exists in system
Event Sourcing
Login operations emit domain events for audit trail and analytics:
Event Type: UserWasLoggedIn
Event Data:
{
"user_id" : str ( UUID ),
"email" : str ,
"aggregate_id" : str ( UUID ),
"event_type" : "UserWasLoggedIn" ,
"timestamp" : datetime
}
Event Flow:
Event raised in UserActions.login() aggregate method
Persisted to event_store table via EventSourcedRepository
Published to PubSub (Redis or in-memory)
Can be consumed by subscribers for:
Login analytics
Security monitoring
Audit logging
User behavior tracking
(((REPLACE_THIS_WITH_IMAGE: cloud-api-login-jwt-flow.png: Sequence diagram showing login flow from credentials submission through JWT token generation and API usage)))
Related Pages
Register Create new user account
Get Current User Retrieve authenticated user details
Refresh Token Obtain new access token
Quick Start Complete authentication tutorial
JWT Flow Guide Understanding token lifecycle
Protected Routes Implement route protection