Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2389bca42b |
Generated
+1
-1
@@ -3467,7 +3467,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "web-petting"
|
||||
version = "0.1.12"
|
||||
version = "0.1.13"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"chrono",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "web-petting"
|
||||
version = "0.1.13"
|
||||
version = "0.1.14"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
+60
-24
@@ -6,6 +6,7 @@ use cot::request::extractors::Path;
|
||||
use cot::response::{IntoResponse, Redirect, Response};
|
||||
use cot::router::{Route, Router};
|
||||
use cot::session::Session;
|
||||
use image::ImageDecoder;
|
||||
use image::ImageFormat;
|
||||
use image::ImageReader;
|
||||
use image::codecs::jpeg::JpegEncoder;
|
||||
@@ -19,6 +20,7 @@ use crate::telegram;
|
||||
|
||||
const SESSION_USER_ID: &str = "user_id";
|
||||
const SESSION_USER_NAME: &str = "user_name";
|
||||
const SESSION_OIDC_STATE: &str = "oidc_state";
|
||||
const MAX_UPLOADED_IMAGE_DIMENSION: u32 = 1920;
|
||||
const UPLOADED_IMAGE_JPEG_QUALITY: u8 = 82;
|
||||
|
||||
@@ -117,9 +119,16 @@ fn transcode_uploaded_image(data: &[u8], ext: &str) -> cot::Result<Option<Vec<u8
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let image = ImageReader::with_format(Cursor::new(data), format)
|
||||
.decode()
|
||||
let mut decoder = ImageReader::with_format(Cursor::new(data), format)
|
||||
.into_decoder()
|
||||
.map_err(|e| cot::Error::internal(e.to_string()))?;
|
||||
let orientation = decoder
|
||||
.orientation()
|
||||
.map_err(|e| cot::Error::internal(e.to_string()))?;
|
||||
let mut image = image::DynamicImage::from_decoder(decoder)
|
||||
.map_err(|e| cot::Error::internal(e.to_string()))?;
|
||||
image.apply_orientation(orientation);
|
||||
|
||||
let resized = image.resize(
|
||||
MAX_UPLOADED_IMAGE_DIMENSION,
|
||||
MAX_UPLOADED_IMAGE_DIMENSION,
|
||||
@@ -640,7 +649,28 @@ fn decode_jwt_payload(token: &str) -> Option<serde_json::Value> {
|
||||
serde_json::from_slice(&bytes).ok()
|
||||
}
|
||||
|
||||
async fn oidc_start(request: Request, db: Database) -> cot::Result<Response> {
|
||||
fn oidc_state_cookie(value: &str, max_age_seconds: u32) -> String {
|
||||
format!(
|
||||
"oidc_state={}; Path=/admin/oidc; HttpOnly; SameSite=Lax; Max-Age={}",
|
||||
value, max_age_seconds,
|
||||
)
|
||||
}
|
||||
|
||||
fn get_cookie(request: &Request, name: &str) -> Option<String> {
|
||||
let prefix = format!("{name}=");
|
||||
request
|
||||
.headers()
|
||||
.get("cookie")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|cookies| {
|
||||
cookies.split(';').find_map(|part| {
|
||||
let part = part.trim();
|
||||
part.strip_prefix(&prefix).map(|v| v.to_string())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async fn oidc_start(request: Request, session: Session, db: Database) -> cot::Result<Response> {
|
||||
let lang = detect_lang(&request);
|
||||
let issuer_url = oidc_setting(&db, "oidc_issuer_url").await?;
|
||||
let client_id = oidc_setting(&db, "oidc_client_id").await?;
|
||||
@@ -666,6 +696,7 @@ async fn oidc_start(request: Request, db: Database) -> cot::Result<Response> {
|
||||
};
|
||||
|
||||
let state = rand_token();
|
||||
session.insert(SESSION_OIDC_STATE, state.clone()).await?;
|
||||
|
||||
let redirect_uri = format!("{}/admin/oidc/callback", site_domain.trim_end_matches('/'));
|
||||
|
||||
@@ -677,10 +708,7 @@ async fn oidc_start(request: Request, db: Database) -> cot::Result<Response> {
|
||||
urlencoding::encode(&state),
|
||||
);
|
||||
|
||||
let state_cookie = format!(
|
||||
"oidc_state={}; Path=/admin/oidc; HttpOnly; Secure; SameSite=Lax; Max-Age=600",
|
||||
state,
|
||||
);
|
||||
let state_cookie = oidc_state_cookie(&state, 600);
|
||||
|
||||
Redirect::new(redirect_url)
|
||||
.into_response()?
|
||||
@@ -692,18 +720,18 @@ async fn oidc_callback(request: Request, session: Session, db: Database) -> cot:
|
||||
let lang = detect_lang(&request);
|
||||
let fail = |code: &str| format!("/admin/login?lang={}&error={}", lang.code(), code);
|
||||
|
||||
// Read saved state from cookie
|
||||
let saved_state = request
|
||||
.headers()
|
||||
.get("cookie")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|cookies| {
|
||||
cookies.split(';').find_map(|part| {
|
||||
let part = part.trim();
|
||||
part.strip_prefix("oidc_state=").map(|v| v.to_string())
|
||||
})
|
||||
})
|
||||
.unwrap_or_default();
|
||||
// Prefer the server-side session; keep the cookie as a compatibility
|
||||
// fallback for flows started before this code was deployed.
|
||||
let saved_state_from_session = session
|
||||
.get::<String>(SESSION_OIDC_STATE)
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
let saved_state_from_cookie = get_cookie(&request, "oidc_state");
|
||||
let saved_state = saved_state_from_session
|
||||
.as_deref()
|
||||
.or(saved_state_from_cookie.as_deref())
|
||||
.unwrap_or("");
|
||||
|
||||
// Extract code and state from query string
|
||||
let query_str = request.uri().query().unwrap_or("");
|
||||
@@ -719,12 +747,20 @@ async fn oidc_callback(request: Request, session: Session, db: Database) -> cot:
|
||||
|
||||
if code.is_empty() || state.is_empty() || state != saved_state {
|
||||
tracing::warn!(
|
||||
"OIDC state mismatch: state={state:?}, saved={saved_state:?}, code_empty={}, state_empty={}",
|
||||
code.is_empty(),
|
||||
state.is_empty(),
|
||||
target: "oidc",
|
||||
has_session_state = saved_state_from_session.is_some(),
|
||||
has_cookie_state = saved_state_from_cookie.is_some(),
|
||||
code_empty = code.is_empty(),
|
||||
state_empty = state.is_empty(),
|
||||
"OIDC state mismatch",
|
||||
);
|
||||
return Redirect::new(fail("sso")).into_response();
|
||||
let clear_cookie = oidc_state_cookie("", 0);
|
||||
return Redirect::new(fail("sso"))
|
||||
.into_response()?
|
||||
.with_header("set-cookie", clear_cookie)
|
||||
.into_response();
|
||||
}
|
||||
let _ = session.remove::<String>(SESSION_OIDC_STATE).await;
|
||||
|
||||
let issuer_url = oidc_setting(&db, "oidc_issuer_url").await?;
|
||||
let client_id = oidc_setting(&db, "oidc_client_id").await?;
|
||||
@@ -886,7 +922,7 @@ async fn oidc_callback(request: Request, session: Session, db: Database) -> cot:
|
||||
session.insert(SESSION_USER_NAME, session_name).await?;
|
||||
|
||||
// Clear the oidc_state cookie
|
||||
let clear_cookie = "oidc_state=; Path=/admin/oidc; HttpOnly; Secure; SameSite=Lax; Max-Age=0";
|
||||
let clear_cookie = oidc_state_cookie("", 0);
|
||||
Redirect::new(format!("/admin/?lang={}", lang.code()))
|
||||
.into_response()?
|
||||
.with_header("set-cookie", clear_cookie)
|
||||
|
||||
Reference in New Issue
Block a user