use super::*;
use crate::auth::browser_token::{cookie_value, BrowserIdentity, BrowserTokenAuthority};
use crate::server::header_escape_guard::HeaderEscapeGuard;
use std::sync::Arc;
const SET_COOKIE: &str = "Set-Cookie";
fn now_unix_secs() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0)
}
fn with_set_cookie(response: HttpResponse, cookie: &str) -> HttpResponse {
match HeaderEscapeGuard::header_value(cookie) {
Ok(value) => response.with_header(SET_COOKIE, value),
Err(_) => json_error(500, "failed to construct refresh cookie"),
}
}
impl RedDBServer {
fn browser_authority(&self) -> Result<Arc<BrowserTokenAuthority>, HttpResponse> {
self.runtime
.browser_token_authority()
.ok_or_else(|| json_error(501, "browser credential layer is not enabled"))
}
pub(crate) fn handle_browser_login(&self, body: Vec<u8>) -> HttpResponse {
let authority = match self.browser_authority() {
Ok(a) => a,
Err(resp) => return resp,
};
let auth_store = match self.runtime.auth_store() {
Some(store) => store,
None => return json_error(501, "authentication is not configured"),
};
let payload = match parse_json_body(&body) {
Ok(value) => value,
Err(resp) => return resp,
};
let username = match json_string_field(&payload, "username") {
Some(u) => u,
None => return json_error(400, "missing 'username' field"),
};
let password = match json_string_field(&payload, "password") {
Some(p) => p,
None => return json_error(400, "missing 'password' field"),
};
let tenant_id = json_string_field(&payload, "tenant_id");
let caller_id = crate::auth::UserId::from_parts(tenant_id.as_deref(), &username);
let session =
match auth_store.authenticate_in_tenant(tenant_id.as_deref(), &username, &password) {
Ok(session) => session,
Err(err) => {
tracing::warn!(
target: "reddb::http_auth",
principal = %caller_id,
"browser login refused"
);
return json_error(401, err.to_string());
}
};
let identity = BrowserIdentity {
username: session.username.clone(),
tenant: session.tenant_id.clone(),
role: session.role,
};
let tokens = match authority.issue(&identity, now_unix_secs()) {
Ok(t) => t,
Err(e) => return json_error(500, format!("failed to issue tokens: {e}")),
};
tracing::info!(
target: "reddb::http_auth",
principal = %caller_id,
"browser login ok"
);
let mut object = Map::new();
object.insert("ok".to_string(), JsonValue::Bool(true));
object.insert(
"access_token".to_string(),
JsonValue::String(tokens.access_token),
);
object.insert("token_type".to_string(), JsonValue::String("Bearer".into()));
object.insert(
"expires_in".to_string(),
JsonValue::Number(tokens.access_expires_in as f64),
);
object.insert(
"username".to_string(),
JsonValue::String(identity.username.clone()),
);
object.insert(
"role".to_string(),
JsonValue::String(identity.role.to_string()),
);
if let Some(t) = &identity.tenant {
object.insert("tenant_id".to_string(), JsonValue::String(t.clone()));
}
let response = json_response(200, JsonValue::Object(object));
with_set_cookie(response, &authority.refresh_cookie(&tokens.refresh_token))
}
pub(crate) fn handle_browser_refresh(
&self,
headers: &BTreeMap<String, String>,
) -> HttpResponse {
let authority = match self.browser_authority() {
Ok(a) => a,
Err(resp) => return resp,
};
let cookie_header = match headers.get("cookie") {
Some(c) => c,
None => return json_error(401, "no refresh cookie"),
};
let refresh = match cookie_value(cookie_header, authority.cookie_name()) {
Some(v) if !v.is_empty() => v,
_ => return json_error(401, "no refresh cookie"),
};
let now = now_unix_secs();
let identity = match authority.validate_refresh(refresh, now) {
Ok(id) => id,
Err(err) => {
tracing::warn!(target: "reddb::http_auth", "browser refresh rejected: {err}");
let response = json_error(401, "invalid or expired refresh token");
return with_set_cookie(response, &authority.clear_cookie());
}
};
let access_token = match authority.issue_access(&identity, now) {
Ok(t) => t,
Err(e) => return json_error(500, format!("failed to issue access token: {e}")),
};
let new_refresh = match authority.issue(&identity, now) {
Ok(t) => t.refresh_token,
Err(e) => return json_error(500, format!("failed to rotate refresh token: {e}")),
};
let mut object = Map::new();
object.insert("ok".to_string(), JsonValue::Bool(true));
object.insert("access_token".to_string(), JsonValue::String(access_token));
object.insert("token_type".to_string(), JsonValue::String("Bearer".into()));
object.insert(
"expires_in".to_string(),
JsonValue::Number(authority.access_ttl_secs() as f64),
);
if let Some(t) = &identity.tenant {
object.insert("tenant_id".to_string(), JsonValue::String(t.clone()));
}
object.insert(
"username".to_string(),
JsonValue::String(identity.username.clone()),
);
object.insert(
"role".to_string(),
JsonValue::String(identity.role.to_string()),
);
let response = json_response(200, JsonValue::Object(object));
with_set_cookie(response, &authority.refresh_cookie(&new_refresh))
}
pub(crate) fn handle_browser_logout(&self) -> HttpResponse {
let authority = match self.browser_authority() {
Ok(a) => a,
Err(resp) => return resp,
};
let response = json_ok("logged out");
with_set_cookie(response, &authority.clear_cookie())
}
}