-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Feature: Implement POST /api/v1/auth/login — User Authentication Endpoint
Problem
Registered FluentMeet users have no way to authenticate. Without a login endpoint, no access or refresh tokens can be issued, making every protected endpoint in the application unreachable. Additionally, storing refresh tokens in a way that is accessible to JavaScript exposes them to XSS attacks; they must be set as HttpOnly cookies by the server.
Proposed Solution
Implement POST /api/v1/auth/login which validates user credentials, issues a short-lived JWT access token and a long-lived JWT refresh token, and returns them in a structured response body. The refresh token is simultaneously set as an HttpOnly, Secure, SameSite=Strict cookie so the browser manages it safely without JavaScript access. The access token is returned in the response body for the client to store in memory (not localStorage).
User Stories
- As a registered user, I want to log in with my email and password and receive an access token, so I can make authenticated API requests.
- As a registered user, I want my refresh token to be stored in an
HttpOnlycookie, so it cannot be stolen by a malicious script running on the page. - As a security engineer, I want login attempts for non-existent users and wrong passwords to return the same generic error, so attackers cannot enumerate valid email addresses via timing or message differences.
- As a developer, I want login to be explicitly blocked for unverified or soft-deleted accounts, with a clear error code for each case, so the client can display a meaningful message.
Acceptance Criteria
POST /api/v1/auth/loginaccepts the following JSON body:{ "email": "user@example.com", "password": "MyStr0ngP@ss!" }- Validation & Authentication:
- If the email does not exist or the password is incorrect, return
401 Unauthorizedwith the same generic message to prevent user enumeration:{ "status": "error", "code": "INVALID_CREDENTIALS", "message": "Invalid email or password.", "details": [] } - If the user exists but
is_verified=False, return403 Forbidden:{ "status": "error", "code": "EMAIL_NOT_VERIFIED", "message": "Please verify your email before logging in.", "details": [] } - If the user exists but
deleted_atis notnull, return403 Forbidden:{ "status": "error", "code": "ACCOUNT_DELETED", "message": "This account has been deleted.", "details": [] }
- If the email does not exist or the password is incorrect, return
- Token Issuance:
- Access Token (AT): signed JWT with
sub(user email),jti(UUID),exp(ACCESS_TOKEN_EXPIRE_MINUTESfrom settings). Returned in the response body. - Refresh Token (RT): signed JWT with
sub(user email),jti(UUID),exp(REFRESH_TOKEN_EXPIRE_DAYSfrom settings). Thejtiis stored in Redis asrefresh_token:{jti}with a TTL equal to the token expiry.
- Access Token (AT): signed JWT with
- On successful login, the response is
200 OK:The refresh token is also set in an{ "access_token": "<jwt>", "refresh_token": "<jwt>", "token_type": "bearer", "expires_in": 900 }HttpOnlycookie:Set-Cookie: refresh_token=<jwt>; HttpOnly; Secure; SameSite=Strict; Path=/api/v1/auth; Max-Age=604800 - The
Pathof the cookie is restricted to/api/v1/authso it is only sent to token-rotation and logout endpoints, not every API request. - The endpoint is rate-limited to 10 requests/minute per IP.
- Passwords are verified using
passlib.context.CryptContext.verify()— never compared in plaintext. - Unit and integration tests cover: successful login, wrong password, unverified account, deleted account, and correct cookie attributes.
Proposed Technical Details
- Router:
app/api/v1/endpoints/auth.py— newPOST /loginroute alongside/signup. - Schema: New
LoginRequest(email: EmailStr, password: str)and reuseTokenschema (already inapp/schemas/user.pywithaccess_token,refresh_token,token_type,expires_in). - Security Logic:
app/core/security.py— implementcreate_access_token(email, jti)andcreate_refresh_token(email, jti)usingpython-jose. - Redis:
app/services/token_store.py—save_refresh_token(jti, ttl)andrevoke_refresh_token(jti)helpers. - Cookie: Use FastAPI's
Responseobject (response.set_cookie(...)) to set theHttpOnlycookie within the route handler. - New/Modified Files:
app/api/v1/endpoints/auth.py— addPOST /login[MODIFY]app/core/security.py—create_access_token,create_refresh_token,verify_password[NEW]app/services/token_store.py— Redis refresh token persistence [NEW]app/schemas/auth.py—LoginRequestschema [MODIFY]
Tasks
- Implement
verify_password,create_access_token, andcreate_refresh_tokeninapp/core/security.py. - Implement
save_refresh_tokenandrevoke_refresh_tokenRedis helpers inapp/services/token_store.py. - Add
LoginRequestPydantic schema toapp/schemas/auth.py. - Implement
POST /api/v1/auth/logininapp/api/v1/endpoints/auth.py. - Apply
ForbiddenExceptionwith codesEMAIL_NOT_VERIFIEDandACCOUNT_DELETEDfor the respective guard conditions. - Set the
HttpOnlyrefresh token cookie with correct attributes in the route response. - Apply
@limiter.limit("10/minute")rate limit to the login route. - Write unit tests for
verify_password,create_access_token, andcreate_refresh_token. - Write integration tests for: correct credentials (
200), wrong password (401), unverified user (403), deleted user (403), and cookie presence/attributes.
Open Questions/Considerations
- Should the access token be returned in the response body only (recommended, stored in memory by the client), or also as a separate cookie? Dual-cookie setups simplify some SPA architectures but complicate CSRF protection.
- Should we enforce a maximum failed login attempt lockout (e.g., lock account after 10 consecutive failures), or rely solely on rate limiting per IP?
- The
ACCESS_TOKEN_EXPIRE_MINUTESdefault is 15 minutes — is this the agreed value, or does the team want a longer-lived AT (e.g., 60 minutes) to reduce/refresh-tokencall frequency? - Should login also return the user's profile (language preferences, avatar URL) in the same response, to save the client a subsequent
GET /users/mecall?