mirror of
https://github.com/joaovitoriasilva/endurain.git
synced 2026-01-10 00:07:57 -05:00
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:
@@ -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,
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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' &&
|
||||||
|
|||||||
Reference in New Issue
Block a user