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": "[email protected]",
"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": "[email protected]",
"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