Refactor auth endpoints to use /auth/* paths

Updated backend and frontend to use '/auth/login', '/auth/refresh', '/auth/mfa/verify', and '/auth/logout' endpoints instead of legacy paths. Adjusted CSRF middleware, route prefixes, tests, documentation, and service utilities to match the new endpoint structure for improved clarity and consistency.
This commit is contained in:
João Vitória Silva
2025-12-18 10:40:59 +00:00
parent f6e06fb3e6
commit 20b1149d3d
7 changed files with 55 additions and 55 deletions

View File

@@ -35,7 +35,7 @@ import core.rate_limit as core_rate_limit
router = APIRouter() router = APIRouter()
@router.post("/token") @router.post("/login")
@core_rate_limit.limiter.limit(core_rate_limit.SESSION_LOGIN_LIMIT) @core_rate_limit.limiter.limit(core_rate_limit.SESSION_LOGIN_LIMIT)
async def login_for_access_token( async def login_for_access_token(
response: Response, response: Response,

View File

@@ -106,9 +106,9 @@ class CSRFMiddleware(BaseHTTPMiddleware):
super().__init__(app) super().__init__(app)
# Define paths that don't need CSRF protection # Define paths that don't need CSRF protection
self.exempt_paths = [ self.exempt_paths = [
"/api/v1/token", "/api/v1/auth/login",
"/api/v1/refresh", "/api/v1/auth/refresh",
"/api/v1/mfa/verify", "/api/v1/auth/mfa/verify",
"/api/v1/password-reset/request", "/api/v1/password-reset/request",
"/api/v1/password-reset/confirm", "/api/v1/password-reset/confirm",
"/api/v1/sign-up/request", "/api/v1/sign-up/request",

View File

@@ -99,7 +99,7 @@ router.include_router(
) )
router.include_router( router.include_router(
auth_router.router, auth_router.router,
prefix=core_config.ROOT_PATH, prefix=core_config.ROOT_PATH + "/auth",
tags=["auth"], tags=["auth"],
) )
router.include_router( router.include_router(

View File

@@ -74,7 +74,7 @@ class TestLoginEndpointSecurity:
) )
resp = fast_api_client.post( resp = fast_api_client.post(
"/token", "/auth/login",
data={"username": "testuser", "password": "secret"}, data={"username": "testuser", "password": "secret"},
headers={"X-Client-Type": client_type}, headers={"X-Client-Type": client_type},
) )
@@ -134,7 +134,7 @@ class TestLoginEndpointSecurity:
mock_mfa.return_value = True mock_mfa.return_value = True
resp = fast_api_client.post( resp = fast_api_client.post(
"/token", "/auth/login",
data={"username": "testuser", "password": "secret"}, data={"username": "testuser", "password": "secret"},
headers={"X-Client-Type": client_type}, headers={"X-Client-Type": client_type},
) )
@@ -154,7 +154,7 @@ class TestLoginEndpointSecurity:
This test sets the application's client type to "desktop" and mocks the authentication, This test sets the application's client type to "desktop" and mocks the authentication,
user activity check, MFA status, token creation, and session creation utilities. It then user activity check, MFA status, token creation, and session creation utilities. It then
sends a POST request to the "/token" endpoint with the "X-Client-Type" header set to "desktop". sends a POST request to the "/auth/login" endpoint with the "X-Client-Type" header set to "desktop".
The test asserts that the response status code is 403 Forbidden and the response detail The test asserts that the response status code is 403 Forbidden and the response detail
indicates an invalid client type. indicates an invalid client type.
@@ -187,7 +187,7 @@ class TestLoginEndpointSecurity:
mock_create_session.return_value = None mock_create_session.return_value = None
resp = fast_api_client.post( resp = fast_api_client.post(
"/token", "/auth/login",
data={"username": "x", "password": "y"}, data={"username": "x", "password": "y"},
headers={"X-Client-Type": "desktop"}, headers={"X-Client-Type": "desktop"},
) )
@@ -236,7 +236,7 @@ class TestLoginEndpointSecurity:
class TestMFAVerifyEndpoint: class TestMFAVerifyEndpoint:
""" """
Test suite for the MFA verification endpoint (/mfa/verify). Test suite for the MFA verification endpoint (/auth/mfa/verify).
This class contains tests that cover various scenarios for the MFA verification endpoint, including: This class contains tests that cover various scenarios for the MFA verification endpoint, including:
- Successful MFA verification and login for different client types (web and mobile). - Successful MFA verification and login for different client types (web and mobile).
@@ -310,7 +310,7 @@ class TestMFAVerifyEndpoint:
) )
resp = fast_api_client.post( resp = fast_api_client.post(
"/mfa/verify", "/auth/mfa/verify",
json={"username": "testuser", "mfa_code": "123456"}, json={"username": "testuser", "mfa_code": "123456"},
headers={"X-Client-Type": client_type}, headers={"X-Client-Type": client_type},
) )
@@ -335,7 +335,7 @@ class TestMFAVerifyEndpoint:
fast_api_app.state.fake_store._store = {} fast_api_app.state.fake_store._store = {}
resp = fast_api_client.post( resp = fast_api_client.post(
"/mfa/verify", "/auth/mfa/verify",
json={"username": "testuser", "mfa_code": "123456"}, json={"username": "testuser", "mfa_code": "123456"},
headers={"X-Client-Type": "web"}, headers={"X-Client-Type": "web"},
) )
@@ -359,7 +359,7 @@ class TestMFAVerifyEndpoint:
mock_verify_mfa.return_value = False mock_verify_mfa.return_value = False
resp = fast_api_client.post( resp = fast_api_client.post(
"/mfa/verify", "/auth/mfa/verify",
json={"username": "testuser", "mfa_code": "999999"}, json={"username": "testuser", "mfa_code": "999999"},
headers={"X-Client-Type": "web"}, headers={"X-Client-Type": "web"},
) )
@@ -389,7 +389,7 @@ class TestMFAVerifyEndpoint:
mock_get_user.return_value = None mock_get_user.return_value = None
resp = fast_api_client.post( resp = fast_api_client.post(
"/mfa/verify", "/auth/mfa/verify",
json={"username": "testuser", "mfa_code": "123456"}, json={"username": "testuser", "mfa_code": "123456"},
headers={"X-Client-Type": "web"}, headers={"X-Client-Type": "web"},
) )
@@ -423,7 +423,7 @@ class TestMFAVerifyEndpoint:
) )
resp = fast_api_client.post( resp = fast_api_client.post(
"/mfa/verify", "/auth/mfa/verify",
json={"username": "inactive", "mfa_code": "123456"}, json={"username": "inactive", "mfa_code": "123456"},
headers={"X-Client-Type": "web"}, headers={"X-Client-Type": "web"},
) )
@@ -434,7 +434,7 @@ class TestMFAVerifyEndpoint:
class TestRefreshTokenEndpoint: class TestRefreshTokenEndpoint:
""" """
Test suite for the refresh token endpoint (/refresh). Test suite for the refresh token endpoint (/auth/refresh).
This class contains tests that cover various scenarios for the token refresh endpoint, including: This class contains tests that cover various scenarios for the token refresh endpoint, including:
- Successful token refresh for different client types (web and mobile). - Successful token refresh for different client types (web and mobile).
@@ -521,7 +521,7 @@ class TestRefreshTokenEndpoint:
fast_api_client.cookies.set("endurain_refresh_token", "refresh_token_value") fast_api_client.cookies.set("endurain_refresh_token", "refresh_token_value")
resp = fast_api_client.post( resp = fast_api_client.post(
"/refresh", "/auth/refresh",
headers={"X-Client-Type": client_type}, headers={"X-Client-Type": client_type},
) )
@@ -552,7 +552,7 @@ class TestRefreshTokenEndpoint:
fast_api_client.cookies.set("endurain_refresh_token", "refresh_token_value") fast_api_client.cookies.set("endurain_refresh_token", "refresh_token_value")
resp = fast_api_client.post( resp = fast_api_client.post(
"/refresh", "/auth/refresh",
headers={"X-Client-Type": "web"}, headers={"X-Client-Type": "web"},
) )
@@ -584,7 +584,7 @@ class TestRefreshTokenEndpoint:
fast_api_client.cookies.set("endurain_refresh_token", "wrong_token_value") fast_api_client.cookies.set("endurain_refresh_token", "wrong_token_value")
resp = fast_api_client.post( resp = fast_api_client.post(
"/refresh", "/auth/refresh",
headers={"X-Client-Type": "web"}, headers={"X-Client-Type": "web"},
) )
@@ -626,7 +626,7 @@ class TestRefreshTokenEndpoint:
fast_api_client.cookies.set("endurain_refresh_token", "refresh_token_value") fast_api_client.cookies.set("endurain_refresh_token", "refresh_token_value")
resp = fast_api_client.post( resp = fast_api_client.post(
"/refresh", "/auth/refresh",
headers={"X-Client-Type": "web"}, headers={"X-Client-Type": "web"},
) )
@@ -680,7 +680,7 @@ class TestRefreshTokenEndpoint:
fast_api_client.cookies.set("endurain_refresh_token", "refresh_token_value") fast_api_client.cookies.set("endurain_refresh_token", "refresh_token_value")
resp = fast_api_client.post( resp = fast_api_client.post(
"/refresh", "/auth/refresh",
headers={"X-Client-Type": "desktop"}, headers={"X-Client-Type": "desktop"},
) )
@@ -690,7 +690,7 @@ class TestRefreshTokenEndpoint:
class TestLogoutEndpoint: class TestLogoutEndpoint:
""" """
Test suite for the logout endpoint (/logout). Test suite for the logout endpoint (/auth/logout).
This class contains tests that cover various scenarios for the logout endpoint, including: This class contains tests that cover various scenarios for the logout endpoint, including:
- Successful logout for different client types (web and mobile). - Successful logout for different client types (web and mobile).
@@ -749,7 +749,7 @@ class TestLogoutEndpoint:
fast_api_client.cookies.set("endurain_refresh_token", "refresh_token_value") fast_api_client.cookies.set("endurain_refresh_token", "refresh_token_value")
resp = fast_api_client.post( resp = fast_api_client.post(
"/logout", "/auth/logout",
headers={"X-Client-Type": client_type}, headers={"X-Client-Type": client_type},
) )
@@ -788,7 +788,7 @@ class TestLogoutEndpoint:
fast_api_client.cookies.set("endurain_refresh_token", "wrong_token_value") fast_api_client.cookies.set("endurain_refresh_token", "wrong_token_value")
resp = fast_api_client.post( resp = fast_api_client.post(
"/logout", "/auth/logout",
headers={"X-Client-Type": "web"}, headers={"X-Client-Type": "web"},
) )
@@ -815,7 +815,7 @@ class TestLogoutEndpoint:
fast_api_client.cookies.set("endurain_refresh_token", "refresh_token_value") fast_api_client.cookies.set("endurain_refresh_token", "refresh_token_value")
resp = fast_api_client.post( resp = fast_api_client.post(
"/logout", "/auth/logout",
headers={"X-Client-Type": "web"}, headers={"X-Client-Type": "web"},
) )
@@ -846,7 +846,7 @@ class TestLogoutEndpoint:
fast_api_client.cookies.set("endurain_refresh_token", "refresh_token_value") fast_api_client.cookies.set("endurain_refresh_token", "refresh_token_value")
resp = fast_api_client.post( resp = fast_api_client.post(
"/logout", "/auth/logout",
headers={"X-Client-Type": "desktop"}, headers={"X-Client-Type": "desktop"},
) )

View File

@@ -23,7 +23,7 @@ Endurain supports integration with other apps through a comprehensive authentica
## Authentication Flows ## Authentication Flows
### Standard Login Flow ### Standard Login Flow
1. Client sends credentials to `/token` endpoint 1. Client sends credentials to `/auth/login` endpoint
2. Backend validates credentials 2. Backend validates credentials
3. If MFA is enabled, backend requests MFA code 3. If MFA is enabled, backend requests MFA code
4. If MFA is disabled or verified, backend generates tokens 4. If MFA is disabled or verified, backend generates tokens
@@ -39,7 +39,7 @@ Endurain supports integration with other apps through a comprehensive authentica
7. User is redirected to the app with active session 7. User is redirected to the app with active session
### Token Refresh Flow ### Token Refresh Flow
1. When access token expires, client sends refresh token to `/refresh` 1. When access token expires, client sends refresh token to `/auth/refresh`
2. Backend validates refresh token and session 2. Backend validates refresh token and session
3. New access token is generated and returned 3. New access token is generated and returned
4. Refresh token may be rotated based on configuration 4. Refresh token may be rotated based on configuration
@@ -51,10 +51,10 @@ The API is reachable under `/api/v1`. Below are the authentication-related endpo
| What | Url | Expected Information | Rate Limit | | What | Url | Expected Information | Rate Limit |
| ---- | --- | -------------------- | ---------- | | ---- | --- | -------------------- | ---------- |
| **Authorize** | `/token` | `FORM` with the fields `username` and `password`. This will be sent in clear text, use of HTTPS is highly recommended | 5 requests/min per IP | | **Authorize** | `/auth/login` | `FORM` with the fields `username` and `password`. This will be sent in clear text, use of HTTPS is highly recommended | 5 requests/min per IP |
| **Refresh Token** | `/refresh` | header `Authorization Bearer: <Refresh Token>` | - | | **Refresh Token** | `/auth/refresh` | header `Authorization Bearer: <Refresh Token>` | - |
| **Verify MFA** | `/mfa/verify` | JSON `{'username': <username>, 'mfa_code': '123456'}` | - | | **Verify MFA** | `/auth/mfa/verify` | JSON `{'username': <username>, 'mfa_code': '123456'}` | - |
| **Logout** | `/logout` | header `Authorization Bearer: <Access Token>` | - | | **Logout** | `/auth/logout` | header `Authorization Bearer: <Access Token>` | - |
### OAuth/SSO Endpoints ### OAuth/SSO Endpoints
@@ -78,11 +78,11 @@ The API is reachable under `/api/v1`. Below are the authentication-related endpo
When Multi-Factor Authentication (MFA) is enabled for a user, the authentication process requires two steps: When Multi-Factor Authentication (MFA) is enabled for a user, the authentication process requires two steps:
### Step 1: Initial Login Request ### Step 1: Initial Login Request
Make a standard login request to `/token`: Make a standard login request to `/auth/login`:
**Request:** **Request:**
```http ```http
POST /api/v1/token POST /api/v1/auth/login
Content-Type: application/x-www-form-urlencoded Content-Type: application/x-www-form-urlencoded
X-Client-Type: web|mobile X-Client-Type: web|mobile
@@ -112,11 +112,11 @@ username=user@example.com&password=userpassword
``` ```
### Step 2: MFA Verification ### Step 2: MFA Verification
Complete the login by providing the MFA code to `/mfa/verify`: Complete the login by providing the MFA code to `/auth/mfa/verify`:
**Request:** **Request:**
```http ```http
POST /api/v1/mfa/verify POST /api/v1/auth/mfa/verify
Content-Type: application/json Content-Type: application/json
X-Client-Type: web|mobile X-Client-Type: web|mobile

View File

@@ -6,22 +6,22 @@ import {
} from '@/utils/serviceUtils' } from '@/utils/serviceUtils'
export const session = { export const session = {
authenticateUser(formData) {
return fetchPostFormUrlEncoded('auth/login', formData)
},
verifyMFAAndLogin(data) {
return fetchPostRequest('auth/mfa/verify', data)
},
logoutUser() {
return fetchPostRequest('auth/logout', null)
},
refreshToken() {
return fetchPostRequest('auth/refresh', null)
},
getUserSessions(userId) { getUserSessions(userId) {
return fetchGetRequest(`sessions/user/${userId}`) return fetchGetRequest(`sessions/user/${userId}`)
}, },
deleteSession(sessionId, userId) { deleteSession(sessionId, userId) {
return fetchDeleteRequest(`sessions/${sessionId}/user/${userId}`) return fetchDeleteRequest(`sessions/${sessionId}/user/${userId}`)
},
authenticateUser(formData) {
return fetchPostFormUrlEncoded('token', formData)
},
verifyMFAAndLogin(data) {
return fetchPostRequest('mfa/verify', data)
},
logoutUser() {
return fetchPostRequest('logout', null)
},
refreshToken() {
return fetchPostRequest('refresh', null)
} }
} }

View File

@@ -21,9 +21,9 @@ function getAccessToken() {
// Helper function to add authorization and CSRF headers // Helper function to add authorization and CSRF headers
function addAuthHeaders(url, options) { function addAuthHeaders(url, options) {
// Add Authorization Bearer header for all authenticated requests // Add Authorization Bearer header for all authenticated requests
// Skip public endpoints (token, password-reset, sign-up) // Skip public endpoints (login, password-reset, sign-up)
if ( if (
url !== 'token' && url !== 'auth/login' &&
url !== 'password-reset/request' && url !== 'password-reset/request' &&
url !== 'password-reset/confirm' && url !== 'password-reset/confirm' &&
url !== 'sign-up/request' && url !== 'sign-up/request' &&
@@ -42,8 +42,8 @@ function addAuthHeaders(url, options) {
// Add CSRF token for state-changing requests // Add CSRF token for state-changing requests
if ( if (
['POST', 'PUT', 'DELETE', 'PATCH'].includes(options.method) && ['POST', 'PUT', 'DELETE', 'PATCH'].includes(options.method) &&
url !== 'token' && url !== 'auth/login' &&
url !== 'mfa/verify' && url !== 'auth/mfa/verify' &&
url !== 'password-reset/request' && url !== 'password-reset/request' &&
url !== 'password-reset/confirm' && url !== 'password-reset/confirm' &&
url !== 'sign-up/request' && url !== 'sign-up/request' &&
@@ -71,9 +71,9 @@ async function fetchWithRetry(url, options, responseType = 'json') {
// Don't retry on 401 for: token, refresh, logout, MFA verify, or Garmin link errors // Don't retry on 401 for: token, refresh, logout, MFA verify, or Garmin link errors
if ( if (
error.message.startsWith('401') && error.message.startsWith('401') &&
url !== 'token' && url !== 'auth/login' &&
url !== 'refresh' && url !== 'auth/refresh' &&
url !== 'logout' url !== 'auth/logout'
) { ) {
if ( if (
url === 'garminconnect/link' && url === 'garminconnect/link' &&