auth_framework/api/
auth.rs

1//! Authentication API Endpoints
2//!
3//! Handles login, logout, token refresh, and related authentication operations
4
5use crate::api::{ApiResponse, ApiState, extract_bearer_token};
6use axum::{Json, extract::State, http::HeaderMap};
7use serde::{Deserialize, Serialize};
8
9/// Login request payload
10#[derive(Debug, Deserialize)]
11pub struct LoginRequest {
12    pub username: String,
13    pub password: String,
14    #[serde(default)]
15    pub mfa_code: Option<String>,
16    #[serde(default)]
17    pub remember_me: bool,
18}
19
20/// Login response data
21#[derive(Debug, Serialize)]
22pub struct LoginResponse {
23    pub access_token: String,
24    pub refresh_token: String,
25    pub token_type: String,
26    pub expires_in: u64,
27    pub user: UserInfo,
28}
29
30/// User information in login response
31#[derive(Debug, Serialize)]
32pub struct UserInfo {
33    pub id: String,
34    pub username: String,
35    pub roles: Vec<String>,
36    pub permissions: Vec<String>,
37}
38
39/// Token refresh request
40#[derive(Debug, Deserialize)]
41pub struct RefreshRequest {
42    pub refresh_token: String,
43}
44
45/// Token refresh response
46#[derive(Debug, Serialize)]
47pub struct RefreshResponse {
48    pub access_token: String,
49    pub token_type: String,
50    pub expires_in: u64,
51}
52
53/// Logout request
54#[derive(Debug, Deserialize)]
55pub struct LogoutRequest {
56    #[serde(default)]
57    pub refresh_token: Option<String>,
58}
59
60/// POST /auth/login
61pub async fn login(
62    State(state): State<ApiState>,
63    Json(req): Json<LoginRequest>,
64) -> ApiResponse<LoginResponse> {
65    // Validate required fields
66    if req.username.is_empty() || req.password.is_empty() {
67        return ApiResponse::validation_error_typed("Username and password are required");
68    }
69
70    // Create credential for authentication
71    let credential = crate::authentication::credentials::Credential::Password {
72        username: req.username.clone(),
73        password: req.password.clone(),
74    };
75
76    // Attempt authentication
77    match state
78        .auth_framework
79        .authenticate("password", credential)
80        .await
81    {
82        Ok(auth_result) => match auth_result {
83            crate::auth::AuthResult::Success(token) => {
84                // Create response with token information
85                let user_info = UserInfo {
86                    id: token.user_id.clone(),
87                    username: req.username,
88                    roles: token.roles.clone(),
89                    permissions: token.permissions.clone(),
90                };
91
92                // Generate actual JWT access token
93                let token_lifetime = std::time::Duration::from_secs(3600); // 1 hour
94                let access_token = match state.auth_framework.token_manager().create_jwt_token(
95                    &token.user_id,
96                    token.permissions.clone(),
97                    Some(token_lifetime),
98                ) {
99                    Ok(jwt) => jwt,
100                    Err(e) => {
101                        tracing::error!("Failed to create JWT token: {}", e);
102                        return ApiResponse::error_typed(
103                            "TOKEN_CREATION_FAILED",
104                            "Failed to create access token",
105                        );
106                    }
107                };
108
109                // Generate refresh token with longer lifetime
110                let refresh_token_lifetime = std::time::Duration::from_secs(86400 * 7); // 7 days
111                let refresh_token = match state.auth_framework.token_manager().create_jwt_token(
112                    &token.user_id,
113                    vec!["refresh".to_string()],
114                    Some(refresh_token_lifetime),
115                ) {
116                    Ok(jwt) => jwt,
117                    Err(e) => {
118                        tracing::error!("Failed to create refresh token: {}", e);
119                        return ApiResponse::error_typed(
120                            "TOKEN_CREATION_FAILED",
121                            "Failed to create refresh token",
122                        );
123                    }
124                };
125
126                let response = LoginResponse {
127                    access_token,
128                    refresh_token,
129                    token_type: "Bearer".to_string(),
130                    expires_in: 3600, // 1 hour
131                    user: user_info,
132                };
133
134                ApiResponse::success(response)
135            }
136            crate::auth::AuthResult::MfaRequired(_challenge) => {
137                // In real implementation, return MFA challenge info
138                ApiResponse::error_typed("MFA_REQUIRED", "Multi-factor authentication required")
139            }
140            crate::auth::AuthResult::Failure(reason) => {
141                ApiResponse::error_typed("AUTHENTICATION_FAILED", reason)
142            }
143        },
144        Err(e) => {
145            // Convert auth error to API error
146            if matches!(e, crate::errors::AuthError::AuthMethod { .. }) {
147                ApiResponse::error_typed("INVALID_CREDENTIALS", "Invalid username or password")
148            } else {
149                ApiResponse::error_typed("AUTH_ERROR", "Authentication failed")
150            }
151        }
152    }
153}
154
155/// POST /auth/refresh
156pub async fn refresh_token(
157    State(state): State<ApiState>,
158    Json(req): Json<RefreshRequest>,
159) -> ApiResponse<RefreshResponse> {
160    if req.refresh_token.is_empty() {
161        return ApiResponse::validation_error_typed("Refresh token is required");
162    }
163
164    // Validate the refresh token
165    match state
166        .auth_framework
167        .token_manager()
168        .validate_jwt_token(&req.refresh_token)
169    {
170        Ok(claims) => {
171            // Check if this is actually a refresh token
172            if !claims.scope.contains("refresh") {
173                return ApiResponse::error_typed("INVALID_TOKEN", "Token is not a refresh token");
174            }
175
176            // Create new access token
177            let token_lifetime = std::time::Duration::from_secs(3600); // 1 hour
178            let new_access_token = match state.auth_framework.token_manager().create_jwt_token(
179                &claims.sub,
180                vec!["read".to_string(), "write".to_string()], // Default permissions
181                Some(token_lifetime),
182            ) {
183                Ok(jwt) => jwt,
184                Err(e) => {
185                    tracing::error!("Failed to create new access token: {}", e);
186                    return ApiResponse::error_typed(
187                        "TOKEN_CREATION_FAILED",
188                        "Failed to create new access token",
189                    );
190                }
191            };
192
193            let response = RefreshResponse {
194                access_token: new_access_token,
195                token_type: "Bearer".to_string(),
196                expires_in: 3600,
197            };
198
199            ApiResponse::success(response)
200        }
201        Err(e) => {
202            tracing::warn!("Invalid refresh token: {}", e);
203            ApiResponse::error_typed("INVALID_TOKEN", "Invalid or expired refresh token")
204        }
205    }
206}
207
208/// POST /auth/logout
209pub async fn logout(
210    State(_state): State<ApiState>,
211    headers: HeaderMap,
212    Json(req): Json<LogoutRequest>,
213) -> ApiResponse<()> {
214    // Extract token from Authorization header
215    if let Some(token) = extract_bearer_token(&headers) {
216        // In a real implementation, invalidate the token
217        tracing::info!("Logging out user with token: {}", &token[..10]);
218    }
219
220    // If refresh token provided, invalidate it too
221    if let Some(ref refresh_token) = req.refresh_token {
222        tracing::info!("Invalidating refresh token: {}", &refresh_token[..10]);
223    }
224
225    ApiResponse::<()>::ok_with_message("Successfully logged out")
226}
227
228/// GET /auth/validate
229/// Validate current token and return user information
230pub async fn validate_token(
231    State(state): State<ApiState>,
232    headers: HeaderMap,
233) -> ApiResponse<UserInfo> {
234    match extract_bearer_token(&headers) {
235        Some(token) => {
236            match crate::api::validate_api_token(&state.auth_framework, &token).await {
237                Ok(auth_token) => {
238                    // Fetch actual user information from storage
239                    let username = match state
240                        .auth_framework
241                        .get_user_profile(&auth_token.user_id)
242                        .await
243                    {
244                        Ok(profile) => profile
245                            .username
246                            .unwrap_or_else(|| format!("user_{}", auth_token.user_id)),
247                        Err(_) => format!("user_{}", auth_token.user_id), // Fallback if profile fetch fails
248                    };
249
250                    let user_info = UserInfo {
251                        id: auth_token.user_id,
252                        username,
253                        roles: auth_token.roles,
254                        permissions: auth_token.permissions,
255                    };
256                    ApiResponse::success(user_info)
257                }
258                Err(_e) => ApiResponse::error_typed("AUTH_ERROR", "Token validation failed"),
259            }
260        }
261        None => ApiResponse::unauthorized_typed(),
262    }
263}
264
265/// GET /auth/providers
266/// List available OAuth providers
267pub async fn list_providers(State(_state): State<ApiState>) -> ApiResponse<Vec<ProviderInfo>> {
268    let providers = vec![
269        ProviderInfo {
270            name: "google".to_string(),
271            display_name: "Google".to_string(),
272            auth_url: "/oauth/google".to_string(),
273        },
274        ProviderInfo {
275            name: "github".to_string(),
276            display_name: "GitHub".to_string(),
277            auth_url: "/oauth/github".to_string(),
278        },
279        ProviderInfo {
280            name: "microsoft".to_string(),
281            display_name: "Microsoft".to_string(),
282            auth_url: "/oauth/microsoft".to_string(),
283        },
284    ];
285
286    ApiResponse::success(providers)
287}
288
289/// Provider information
290#[derive(Debug, Serialize)]
291pub struct ProviderInfo {
292    pub name: String,
293    pub display_name: String,
294    pub auth_url: String,
295}
296
297