Added OIDC auth
Build and Publish / Build and Publish Docker Image (push) Successful in 2m53s

This commit is contained in:
2026-05-19 00:32:36 +03:00
parent fd1e78ba8c
commit 4d9d0a894c
2 changed files with 32 additions and 19 deletions
+29 -17
View File
@@ -531,8 +531,6 @@ async fn logout(request: Request, session: Session) -> cot::Result<Response> {
// OIDC Handlers
// ---------------------------------------------------------------------------
const SESSION_OIDC_STATE: &str = "oidc_state";
/// Read an OIDC-related setting from the DB, returning empty string if absent.
async fn oidc_setting(db: &Database, name: &str) -> cot::Result<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()
}
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 issuer_url = oidc_setting(&db, "oidc_issuer_url").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();
session.insert(SESSION_OIDC_STATE, state.clone()).await?;
let redirect_uri = format!(
"{}/admin/oidc/callback",
@@ -603,13 +600,34 @@ async fn oidc_start(request: Request, session: Session, db: Database) -> cot::Re
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> {
let lang = detect_lang(&request);
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
let query_str = request.uri().query().unwrap_or("");
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 {
tracing::warn!(
"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();
}
// Clear used state
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?;
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_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
View File
@@ -11,8 +11,8 @@ use tracing_subscriber;
use cot::cli::CliMetadata;
use cot::config::{
DatabaseConfig, MiddlewareConfig, ProjectConfig, SessionMiddlewareConfig, SessionStoreConfig,
SessionStoreTypeConfig,
DatabaseConfig, MiddlewareConfig, ProjectConfig, SameSite, SessionMiddlewareConfig,
SessionStoreConfig, SessionStoreTypeConfig,
};
use cot::db::migrations::SyncDynMigration;
use cot::middleware::SessionMiddleware;
@@ -69,6 +69,7 @@ impl Project for PettingProject {
.session(
SessionMiddlewareConfig::builder()
.secure(false)
.same_site(SameSite::Lax)
.store(
SessionStoreConfig::builder()
.store_type(SessionStoreTypeConfig::Database)