diff --git a/backend/app/auth/router.py b/backend/app/auth/router.py index 067c926c7..95b545bd9 100644 --- a/backend/app/auth/router.py +++ b/backend/app/auth/router.py @@ -35,7 +35,7 @@ import core.rate_limit as core_rate_limit router = APIRouter() -@router.post("/token") +@router.post("/login") @core_rate_limit.limiter.limit(core_rate_limit.SESSION_LOGIN_LIMIT) async def login_for_access_token( response: Response, diff --git a/backend/app/core/middleware.py b/backend/app/core/middleware.py index 635610cb7..7406b9f02 100644 --- a/backend/app/core/middleware.py +++ b/backend/app/core/middleware.py @@ -106,9 +106,9 @@ class CSRFMiddleware(BaseHTTPMiddleware): super().__init__(app) # Define paths that don't need CSRF protection self.exempt_paths = [ - "/api/v1/token", - "/api/v1/refresh", - "/api/v1/mfa/verify", + "/api/v1/auth/login", + "/api/v1/auth/refresh", + "/api/v1/auth/mfa/verify", "/api/v1/password-reset/request", "/api/v1/password-reset/confirm", "/api/v1/sign-up/request", diff --git a/backend/app/core/routes.py b/backend/app/core/routes.py index 0cde2961e..faf2140d6 100644 --- a/backend/app/core/routes.py +++ b/backend/app/core/routes.py @@ -99,7 +99,7 @@ router.include_router( ) router.include_router( auth_router.router, - prefix=core_config.ROOT_PATH, + prefix=core_config.ROOT_PATH + "/auth", tags=["auth"], ) router.include_router( diff --git a/backend/tests/session/test_router.py b/backend/tests/session/test_router.py index 8660ef6a5..fd5950259 100644 --- a/backend/tests/session/test_router.py +++ b/backend/tests/session/test_router.py @@ -74,7 +74,7 @@ class TestLoginEndpointSecurity: ) resp = fast_api_client.post( - "/token", + "/auth/login", data={"username": "testuser", "password": "secret"}, headers={"X-Client-Type": client_type}, ) @@ -134,7 +134,7 @@ class TestLoginEndpointSecurity: mock_mfa.return_value = True resp = fast_api_client.post( - "/token", + "/auth/login", data={"username": "testuser", "password": "secret"}, 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, 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 indicates an invalid client type. @@ -187,7 +187,7 @@ class TestLoginEndpointSecurity: mock_create_session.return_value = None resp = fast_api_client.post( - "/token", + "/auth/login", data={"username": "x", "password": "y"}, headers={"X-Client-Type": "desktop"}, ) @@ -236,7 +236,7 @@ class TestLoginEndpointSecurity: 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: - Successful MFA verification and login for different client types (web and mobile). @@ -310,7 +310,7 @@ class TestMFAVerifyEndpoint: ) resp = fast_api_client.post( - "/mfa/verify", + "/auth/mfa/verify", json={"username": "testuser", "mfa_code": "123456"}, headers={"X-Client-Type": client_type}, ) @@ -335,7 +335,7 @@ class TestMFAVerifyEndpoint: fast_api_app.state.fake_store._store = {} resp = fast_api_client.post( - "/mfa/verify", + "/auth/mfa/verify", json={"username": "testuser", "mfa_code": "123456"}, headers={"X-Client-Type": "web"}, ) @@ -359,7 +359,7 @@ class TestMFAVerifyEndpoint: mock_verify_mfa.return_value = False resp = fast_api_client.post( - "/mfa/verify", + "/auth/mfa/verify", json={"username": "testuser", "mfa_code": "999999"}, headers={"X-Client-Type": "web"}, ) @@ -389,7 +389,7 @@ class TestMFAVerifyEndpoint: mock_get_user.return_value = None resp = fast_api_client.post( - "/mfa/verify", + "/auth/mfa/verify", json={"username": "testuser", "mfa_code": "123456"}, headers={"X-Client-Type": "web"}, ) @@ -423,7 +423,7 @@ class TestMFAVerifyEndpoint: ) resp = fast_api_client.post( - "/mfa/verify", + "/auth/mfa/verify", json={"username": "inactive", "mfa_code": "123456"}, headers={"X-Client-Type": "web"}, ) @@ -434,7 +434,7 @@ class TestMFAVerifyEndpoint: 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: - 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") resp = fast_api_client.post( - "/refresh", + "/auth/refresh", headers={"X-Client-Type": client_type}, ) @@ -552,7 +552,7 @@ class TestRefreshTokenEndpoint: fast_api_client.cookies.set("endurain_refresh_token", "refresh_token_value") resp = fast_api_client.post( - "/refresh", + "/auth/refresh", headers={"X-Client-Type": "web"}, ) @@ -584,7 +584,7 @@ class TestRefreshTokenEndpoint: fast_api_client.cookies.set("endurain_refresh_token", "wrong_token_value") resp = fast_api_client.post( - "/refresh", + "/auth/refresh", headers={"X-Client-Type": "web"}, ) @@ -626,7 +626,7 @@ class TestRefreshTokenEndpoint: fast_api_client.cookies.set("endurain_refresh_token", "refresh_token_value") resp = fast_api_client.post( - "/refresh", + "/auth/refresh", headers={"X-Client-Type": "web"}, ) @@ -680,7 +680,7 @@ class TestRefreshTokenEndpoint: fast_api_client.cookies.set("endurain_refresh_token", "refresh_token_value") resp = fast_api_client.post( - "/refresh", + "/auth/refresh", headers={"X-Client-Type": "desktop"}, ) @@ -690,7 +690,7 @@ class TestRefreshTokenEndpoint: 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: - 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") resp = fast_api_client.post( - "/logout", + "/auth/logout", headers={"X-Client-Type": client_type}, ) @@ -788,7 +788,7 @@ class TestLogoutEndpoint: fast_api_client.cookies.set("endurain_refresh_token", "wrong_token_value") resp = fast_api_client.post( - "/logout", + "/auth/logout", headers={"X-Client-Type": "web"}, ) @@ -815,7 +815,7 @@ class TestLogoutEndpoint: fast_api_client.cookies.set("endurain_refresh_token", "refresh_token_value") resp = fast_api_client.post( - "/logout", + "/auth/logout", headers={"X-Client-Type": "web"}, ) @@ -846,7 +846,7 @@ class TestLogoutEndpoint: fast_api_client.cookies.set("endurain_refresh_token", "refresh_token_value") resp = fast_api_client.post( - "/logout", + "/auth/logout", headers={"X-Client-Type": "desktop"}, ) diff --git a/docs/developer-guide/authentication.md b/docs/developer-guide/authentication.md index 40d68147c..e3f551f09 100644 --- a/docs/developer-guide/authentication.md +++ b/docs/developer-guide/authentication.md @@ -23,7 +23,7 @@ Endurain supports integration with other apps through a comprehensive authentica ## Authentication Flows ### Standard Login Flow -1. Client sends credentials to `/token` endpoint +1. Client sends credentials to `/auth/login` endpoint 2. Backend validates credentials 3. If MFA is enabled, backend requests MFA code 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 ### 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 3. New access token is generated and returned 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 | | ---- | --- | -------------------- | ---------- | -| **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 | -| **Refresh Token** | `/refresh` | header `Authorization Bearer: ` | - | -| **Verify MFA** | `/mfa/verify` | JSON `{'username': , 'mfa_code': '123456'}` | - | -| **Logout** | `/logout` | header `Authorization Bearer: ` | - | +| **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** | `/auth/refresh` | header `Authorization Bearer: ` | - | +| **Verify MFA** | `/auth/mfa/verify` | JSON `{'username': , 'mfa_code': '123456'}` | - | +| **Logout** | `/auth/logout` | header `Authorization Bearer: ` | - | ### 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: ### Step 1: Initial Login Request -Make a standard login request to `/token`: +Make a standard login request to `/auth/login`: **Request:** ```http -POST /api/v1/token +POST /api/v1/auth/login Content-Type: application/x-www-form-urlencoded X-Client-Type: web|mobile @@ -112,11 +112,11 @@ username=user@example.com&password=userpassword ``` ### 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:** ```http -POST /api/v1/mfa/verify +POST /api/v1/auth/mfa/verify Content-Type: application/json X-Client-Type: web|mobile diff --git a/frontend/app/src/services/sessionService.js b/frontend/app/src/services/sessionService.js index 469df9e60..bd00c8a41 100644 --- a/frontend/app/src/services/sessionService.js +++ b/frontend/app/src/services/sessionService.js @@ -6,22 +6,22 @@ import { } from '@/utils/serviceUtils' 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) { return fetchGetRequest(`sessions/user/${userId}`) }, deleteSession(sessionId, 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) } } diff --git a/frontend/app/src/utils/serviceUtils.js b/frontend/app/src/utils/serviceUtils.js index 0764aabe5..066aed6db 100644 --- a/frontend/app/src/utils/serviceUtils.js +++ b/frontend/app/src/utils/serviceUtils.js @@ -21,9 +21,9 @@ function getAccessToken() { // Helper function to add authorization and CSRF headers function addAuthHeaders(url, options) { // 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 ( - url !== 'token' && + url !== 'auth/login' && url !== 'password-reset/request' && url !== 'password-reset/confirm' && url !== 'sign-up/request' && @@ -42,8 +42,8 @@ function addAuthHeaders(url, options) { // Add CSRF token for state-changing requests if ( ['POST', 'PUT', 'DELETE', 'PATCH'].includes(options.method) && - url !== 'token' && - url !== 'mfa/verify' && + url !== 'auth/login' && + url !== 'auth/mfa/verify' && url !== 'password-reset/request' && url !== 'password-reset/confirm' && 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 if ( error.message.startsWith('401') && - url !== 'token' && - url !== 'refresh' && - url !== 'logout' + url !== 'auth/login' && + url !== 'auth/refresh' && + url !== 'auth/logout' ) { if ( url === 'garminconnect/link' &&