fix(api): add refresh
This commit is contained in:
@@ -25,6 +25,11 @@ struct Code2TokenRequest {
|
|||||||
client_secret: String,
|
client_secret: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, serde::Serialize)]
|
||||||
|
struct RefreshTokenRequest {
|
||||||
|
refresh_token: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct Code2TokenData {
|
struct Code2TokenData {
|
||||||
@@ -43,7 +48,9 @@ struct AppResponse<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn router() -> Router<AppState> {
|
pub fn router() -> Router<AppState> {
|
||||||
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 {
|
fn is_https(headers: &axum::http::HeaderMap) -> bool {
|
||||||
@@ -74,6 +81,115 @@ fn cookie_header(
|
|||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct RefreshTokenQuery {
|
||||||
|
pub token: String,
|
||||||
|
pub next: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn refresh_token_handler(
|
||||||
|
headers: axum::http::HeaderMap,
|
||||||
|
Query(q): Query<RefreshTokenQuery>,
|
||||||
|
) -> Result<axum::response::Response, AppError> {
|
||||||
|
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::<AppResponse<RefreshResponseData>>()
|
||||||
|
.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>) -> String {
|
fn resolve_front_redirect(next: Option<String>) -> String {
|
||||||
let base = std::env::var("CMS_FRONT_BASE_URL").ok();
|
let base = std::env::var("CMS_FRONT_BASE_URL").ok();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user