This commit is contained in:
+29
-17
@@ -531,8 +531,6 @@ async fn logout(request: Request, session: Session) -> cot::Result<Response> {
|
|||||||
// OIDC Handlers
|
// OIDC Handlers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const SESSION_OIDC_STATE: &str = "oidc_state";
|
|
||||||
|
|
||||||
/// Read an OIDC-related setting from the DB, returning empty string if absent.
|
/// Read an OIDC-related setting from the DB, returning empty string if absent.
|
||||||
async fn oidc_setting(db: &Database, name: &str) -> cot::Result<String> {
|
async fn oidc_setting(db: &Database, name: &str) -> cot::Result<String> {
|
||||||
let k = name.to_string();
|
let k = name.to_string();
|
||||||
@@ -568,7 +566,7 @@ fn decode_jwt_payload(token: &str) -> Option<serde_json::Value> {
|
|||||||
serde_json::from_slice(&bytes).ok()
|
serde_json::from_slice(&bytes).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn oidc_start(request: Request, session: Session, db: Database) -> cot::Result<Response> {
|
async fn oidc_start(request: Request, db: Database) -> cot::Result<Response> {
|
||||||
let lang = detect_lang(&request);
|
let lang = detect_lang(&request);
|
||||||
let issuer_url = oidc_setting(&db, "oidc_issuer_url").await?;
|
let issuer_url = oidc_setting(&db, "oidc_issuer_url").await?;
|
||||||
let client_id = oidc_setting(&db, "oidc_client_id").await?;
|
let client_id = oidc_setting(&db, "oidc_client_id").await?;
|
||||||
@@ -588,7 +586,6 @@ async fn oidc_start(request: Request, session: Session, db: Database) -> cot::Re
|
|||||||
};
|
};
|
||||||
|
|
||||||
let state = rand_token();
|
let state = rand_token();
|
||||||
session.insert(SESSION_OIDC_STATE, state.clone()).await?;
|
|
||||||
|
|
||||||
let redirect_uri = format!(
|
let redirect_uri = format!(
|
||||||
"{}/admin/oidc/callback",
|
"{}/admin/oidc/callback",
|
||||||
@@ -603,13 +600,34 @@ async fn oidc_start(request: Request, session: Session, db: Database) -> cot::Re
|
|||||||
urlencoding::encode(&state),
|
urlencoding::encode(&state),
|
||||||
);
|
);
|
||||||
|
|
||||||
Redirect::new(redirect_url).into_response()
|
let state_cookie = format!(
|
||||||
|
"oidc_state={}; Path=/admin/oidc; HttpOnly; Secure; SameSite=Lax; Max-Age=600",
|
||||||
|
state,
|
||||||
|
);
|
||||||
|
|
||||||
|
Redirect::new(redirect_url)
|
||||||
|
.into_response()?
|
||||||
|
.with_header("set-cookie", state_cookie)
|
||||||
|
.into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn oidc_callback(request: Request, session: Session, db: Database) -> cot::Result<Response> {
|
async fn oidc_callback(request: Request, session: Session, db: Database) -> cot::Result<Response> {
|
||||||
let lang = detect_lang(&request);
|
let lang = detect_lang(&request);
|
||||||
let fail = format!("/admin/login?lang={}&error=sso", lang.code());
|
let fail = format!("/admin/login?lang={}&error=sso", lang.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();
|
||||||
|
|
||||||
// Extract code and state from query string
|
// Extract code and state from query string
|
||||||
let query_str = request.uri().query().unwrap_or("");
|
let query_str = request.uri().query().unwrap_or("");
|
||||||
let mut code = String::new();
|
let mut code = String::new();
|
||||||
@@ -622,14 +640,6 @@ async fn oidc_callback(request: Request, session: Session, db: Database) -> cot:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify state
|
|
||||||
let saved_state = session
|
|
||||||
.get::<String>(SESSION_OIDC_STATE)
|
|
||||||
.await
|
|
||||||
.ok()
|
|
||||||
.flatten()
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
if code.is_empty() || state.is_empty() || state != saved_state {
|
if code.is_empty() || state.is_empty() || state != saved_state {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
"OIDC state mismatch: state={state:?}, saved={saved_state:?}, code_empty={}, state_empty={}",
|
"OIDC state mismatch: state={state:?}, saved={saved_state:?}, code_empty={}, state_empty={}",
|
||||||
@@ -639,9 +649,6 @@ async fn oidc_callback(request: Request, session: Session, db: Database) -> cot:
|
|||||||
return Redirect::new(fail).into_response();
|
return Redirect::new(fail).into_response();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear used state
|
|
||||||
let _ = session.remove::<String>(SESSION_OIDC_STATE).await;
|
|
||||||
|
|
||||||
let issuer_url = oidc_setting(&db, "oidc_issuer_url").await?;
|
let issuer_url = oidc_setting(&db, "oidc_issuer_url").await?;
|
||||||
let client_id = oidc_setting(&db, "oidc_client_id").await?;
|
let client_id = oidc_setting(&db, "oidc_client_id").await?;
|
||||||
let client_secret = oidc_setting(&db, "oidc_client_secret").await?;
|
let client_secret = oidc_setting(&db, "oidc_client_secret").await?;
|
||||||
@@ -753,7 +760,12 @@ async fn oidc_callback(request: Request, session: Session, db: Database) -> cot:
|
|||||||
session.insert(SESSION_USER_ID, user.id.unwrap()).await?;
|
session.insert(SESSION_USER_ID, user.id.unwrap()).await?;
|
||||||
session.insert(SESSION_USER_NAME, display).await?;
|
session.insert(SESSION_USER_NAME, display).await?;
|
||||||
|
|
||||||
Redirect::new(format!("/admin/?lang={}", lang.code())).into_response()
|
// Clear the oidc_state cookie
|
||||||
|
let clear_cookie = "oidc_state=; Path=/admin/oidc; HttpOnly; Secure; SameSite=Lax; Max-Age=0";
|
||||||
|
Redirect::new(format!("/admin/?lang={}", lang.code()))
|
||||||
|
.into_response()?
|
||||||
|
.with_header("set-cookie", clear_cookie)
|
||||||
|
.into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
+3
-2
@@ -11,8 +11,8 @@ use tracing_subscriber;
|
|||||||
|
|
||||||
use cot::cli::CliMetadata;
|
use cot::cli::CliMetadata;
|
||||||
use cot::config::{
|
use cot::config::{
|
||||||
DatabaseConfig, MiddlewareConfig, ProjectConfig, SessionMiddlewareConfig, SessionStoreConfig,
|
DatabaseConfig, MiddlewareConfig, ProjectConfig, SameSite, SessionMiddlewareConfig,
|
||||||
SessionStoreTypeConfig,
|
SessionStoreConfig, SessionStoreTypeConfig,
|
||||||
};
|
};
|
||||||
use cot::db::migrations::SyncDynMigration;
|
use cot::db::migrations::SyncDynMigration;
|
||||||
use cot::middleware::SessionMiddleware;
|
use cot::middleware::SessionMiddleware;
|
||||||
@@ -69,6 +69,7 @@ impl Project for PettingProject {
|
|||||||
.session(
|
.session(
|
||||||
SessionMiddlewareConfig::builder()
|
SessionMiddlewareConfig::builder()
|
||||||
.secure(false)
|
.secure(false)
|
||||||
|
.same_site(SameSite::Lax)
|
||||||
.store(
|
.store(
|
||||||
SessionStoreConfig::builder()
|
SessionStoreConfig::builder()
|
||||||
.store_type(SessionStoreTypeConfig::Database)
|
.store_type(SessionStoreTypeConfig::Database)
|
||||||
|
|||||||
Reference in New Issue
Block a user