diff --git a/src/api/handlers/auth.rs b/src/api/handlers/auth.rs index 6dddf8b..068e593 100644 --- a/src/api/handlers/auth.rs +++ b/src/api/handlers/auth.rs @@ -25,6 +25,11 @@ struct Code2TokenRequest { client_secret: String, } +#[derive(Debug, Deserialize, serde::Serialize)] +struct RefreshTokenRequest { + refresh_token: String, +} + #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct Code2TokenData { @@ -43,7 +48,9 @@ struct AppResponse { } pub fn router() -> Router { - Router::new().route("/callback", get(sso_callback_handler)) + Router::new() + .route("/callback", get(sso_callback_handler)) + .route("/refresh", get(refresh_token_handler)) } fn is_https(headers: &axum::http::HeaderMap) -> bool { @@ -74,6 +81,115 @@ fn cookie_header( s } +#[derive(Debug, Deserialize)] +pub struct RefreshTokenQuery { + pub token: String, + pub next: Option, +} + +pub async fn refresh_token_handler( + headers: axum::http::HeaderMap, + Query(q): Query, +) -> Result { + if q.token.trim().is_empty() { + return Ok(Redirect::temporary("/auth-error?message=missing_token").into_response()); + } + + let iam_base = std::env::var("IAM_BASE_URL") + .or_else(|_| std::env::var("IAM_SERVICE_BASE_URL")) + .map_err(|_| AppError::ConfigError("IAM_BASE_URL is required".into()))?; + + let http = reqwest::Client::new(); + let resp = http + .post(format!( + "{}/api/v1/auth/refresh", + iam_base.trim_end_matches('/') + )) + .json(&RefreshTokenRequest { + refresh_token: q.token, + }) + .send() + .await + .map_err(|e| AppError::AnyhowError(anyhow::anyhow!(e)))?; + + let status = resp.status(); + // Assuming IAM service returns the same structure as LoginResponse which Code2TokenData roughly matches + // But LoginResponse structure is: access_token, refresh_token, token_type, expires_in. + // Code2TokenData has tenant_id, user_id extra? + // Let's check IAM service LoginResponse definition. + // IAM Service LoginResponse: access_token, refresh_token, token_type, expires_in. + // Wait, Code2TokenData expects tenant_id and user_id. + // Does IAM refresh endpoint return tenant_id and user_id? + // IAM Service LoginResponse struct in src/models.rs (iam-service) DOES NOT have tenant_id/user_id. + // So we cannot reuse Code2TokenData for refresh response parsing if we expect those fields. + // But usually refresh token response just updates access_token (and maybe refresh_token). + // TenantId and UserId should not change. We can keep existing cookies for them if we don't have them. + // But wait, we are setting cookies. If we don't get tenant_id/user_id, we can't set them (or we re-set them if we knew them). + // The previous cookies are still there. We just need to update access_token and refresh_token. + + // Let's define a separate struct for Refresh Response if needed. + #[derive(Debug, Deserialize)] + #[serde(rename_all = "camelCase")] + struct RefreshResponseData { + access_token: String, + refresh_token: String, + expires_in: usize, + } + + let body = resp + .json::>() + .await + .map_err(|e| AppError::AnyhowError(anyhow::anyhow!(e)))?; + + if !status.is_success() || body.code != 0 { + // Refresh failed, redirect to login + let login_url = resolve_front_redirect(q.next); // Actually redirect to front login page or handle error + // If refresh fails, we probably want to redirect to the original requested page so it can trigger login flow, + // OR redirect to auth-error. + // But the middleware calls this. If this returns redirect, the middleware will return redirect. + // If middleware sees error, it should redirect to login. + return Ok(Redirect::temporary("/auth-error?message=refresh_failed").into_response()); + } + + let Some(data) = body.data else { + return Ok(Redirect::temporary("/auth-error?message=invalid_refresh_response").into_response()); + }; + + let target = resolve_front_redirect(q.next); + let secure = is_https(&headers); + let mut res = Redirect::temporary(&target).into_response(); + + let refresh_max_age = 30_u64 * 24 * 60 * 60; + + res.headers_mut().append( + header::SET_COOKIE, + HeaderValue::from_str(&cookie_header( + "accessToken", + &data.access_token, + secure, + true, + Some(data.expires_in as u64), + )) + .map_err(|e| AppError::AnyhowError(anyhow::anyhow!(e)))?, + ); + res.headers_mut().append( + header::SET_COOKIE, + HeaderValue::from_str(&cookie_header( + "refreshToken", + &data.refresh_token, + secure, + true, + Some(refresh_max_age), + )) + .map_err(|e| AppError::AnyhowError(anyhow::anyhow!(e)))?, + ); + + // We don't update tenantId/userId as we don't get them from refresh endpoint usually. + // They should persist. + + Ok(res) +} + fn resolve_front_redirect(next: Option) -> String { let base = std::env::var("CMS_FRONT_BASE_URL").ok();