Skip to content

Commit a451c7e

Browse files
lovasoacursoragent
andauthored
Fix concurrent oidc login cookie overwrites (#1018)
* Refactor OIDC state handling to use separate nonce and redirect cookies Co-authored-by: contact <[email protected]> * Refactor OIDC state management to use separate nonce and redirect cookies Co-authored-by: contact <[email protected]> * Refactor OIDC redirect handling to use updated cookie names and improve error logging * Update OIDC redirect response to use header constant and simplify cookie creation * Refactor nonce retrieval in OIDC user info handling to simplify logic and ensure proper token claims processing * Refactor OIDC cookie handling to introduce a constant for cookie expiration and simplify nonce validation logic * Refactor OIDC redirect URL cookie retrieval to streamline logic and improve error handling * Refactor OIDC redirect URL cookie name construction for improved clarity * fix redirect auth cookie removal Update OIDC redirect URL cookie path to ensure proper handling after authentication * Refactor OIDC redirect URL cookie name construction for improved efficiency --------- Co-authored-by: Cursor Agent <[email protected]>
1 parent 31a4c29 commit a451c7e

File tree

1 file changed

+72
-78
lines changed

1 file changed

+72
-78
lines changed

src/webserver/oidc.rs

Lines changed: 72 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use std::{future::Future, pin::Pin, str::FromStr, sync::Arc};
66

77
use crate::webserver::http_client::get_http_client_from_appdata;
88
use crate::{app_config::AppConfig, AppState};
9+
use actix_web::http::header;
910
use actix_web::{
1011
body::BoxBody,
1112
cookie::Cookie,
@@ -16,7 +17,6 @@ use actix_web::{
1617
};
1718
use anyhow::{anyhow, Context};
1819
use awc::Client;
19-
use chrono::Utc;
2020
use openidconnect::core::{
2121
CoreAuthDisplay, CoreAuthPrompt, CoreErrorResponseType, CoreGenderClaim, CoreJsonWebKey,
2222
CoreJweContentEncryptionAlgorithm, CoreJwsSigningAlgorithm, CoreRevocableToken,
@@ -40,9 +40,12 @@ type LocalBoxFuture<T> = Pin<Box<dyn Future<Output = T> + 'static>>;
4040

4141
const SQLPAGE_AUTH_COOKIE_NAME: &str = "sqlpage_auth";
4242
const SQLPAGE_REDIRECT_URI: &str = "/sqlpage/oidc_callback";
43-
const SQLPAGE_STATE_COOKIE_NAME: &str = "sqlpage_oidc_state";
43+
const SQLPAGE_NONCE_COOKIE_NAME: &str = "sqlpage_oidc_nonce";
44+
const SQLPAGE_REDIRECT_URL_COOKIE_PREFIX: &str = "sqlpage_oidc_redirect_url_";
4445
const OIDC_CLIENT_MAX_REFRESH_INTERVAL: Duration = Duration::from_secs(60 * 60);
4546
const OIDC_CLIENT_MIN_REFRESH_INTERVAL: Duration = Duration::from_secs(5);
47+
const AUTH_COOKIE_EXPIRATION: awc::cookie::time::Duration =
48+
actix_web::cookie::time::Duration::days(7);
4649

4750
#[derive(Clone, Debug, Serialize, Deserialize)]
4851
#[serde(transparent)]
@@ -209,11 +212,11 @@ impl OidcState {
209212
async fn get_token_claims(
210213
&self,
211214
id_token: OidcToken,
212-
state: Option<&OidcLoginState>,
215+
expected_nonce: &Nonce,
213216
) -> anyhow::Result<OidcClaims> {
214217
let client = &self.get_client().await;
215218
let verifier = self.config.create_id_token_verifier(client);
216-
let nonce_verifier = |nonce: Option<&Nonce>| check_nonce(nonce, state);
219+
let nonce_verifier = |nonce: Option<&Nonce>| check_nonce(nonce, expected_nonce);
217220
let claims: OidcClaims = id_token
218221
.into_claims(&verifier, nonce_verifier)
219222
.map_err(|e| anyhow::anyhow!("Could not verify the ID token: {}", e))?;
@@ -366,7 +369,7 @@ async fn handle_unauthenticated_request(
366369
log::debug!("Redirecting to OIDC provider");
367370

368371
let initial_url = request.uri().to_string();
369-
let response = build_auth_provider_redirect_response(oidc_state, initial_url).await;
372+
let response = build_auth_provider_redirect_response(oidc_state, &initial_url).await;
370373
MiddlewareResponse::Respond(request.into_response(response))
371374
}
372375

@@ -375,22 +378,28 @@ async fn handle_oidc_callback(oidc_state: &OidcState, request: ServiceRequest) -
375378
match process_oidc_callback(oidc_state, query_string, &request).await {
376379
Ok(response) => request.into_response(response),
377380
Err(e) => {
378-
let redirect_url =
379-
get_state_from_cookie(&request).map_or_else(|_| "/".into(), |s| s.initial_url);
380-
log::error!("Failed to process OIDC callback. Refreshing oidc provider metadata, then redirecting to {redirect_url}: {e:#}");
381+
log::error!("Failed to process OIDC callback. Refreshing oidc provider metadata, then redirecting to home page: {e:#}");
381382
oidc_state.refresh_on_error(&request).await;
382-
let resp = build_auth_provider_redirect_response(oidc_state, redirect_url).await;
383+
let resp = build_auth_provider_redirect_response(oidc_state, "/").await;
383384
request.into_response(resp)
384385
}
385386
}
386387
}
387388

388389
/// When an user has already authenticated (potentially in another tab), we ignore the callback and redirect to the initial URL.
389390
fn handle_authenticated_oidc_callback(request: ServiceRequest) -> ServiceResponse {
390-
let redirect_url = match get_state_from_cookie(&request) {
391-
Ok(state) => state.initial_url,
392-
Err(_) => "/".to_string(),
393-
};
391+
// Try to get redirect URL from query params if available
392+
let redirect_url = Query::<OidcCallbackParams>::from_query(request.query_string())
393+
.with_context(|| "Failed to parse OIDC callback parameters in authenticated callback")
394+
.and_then(|params| get_redirect_url_cookie(&request, &params.state))
395+
.map_or_else(
396+
|e| {
397+
log::warn!("No redirect URL cookie: {e:#}");
398+
"/".to_string()
399+
},
400+
|cookie| cookie.value().to_string(),
401+
);
402+
394403
log::debug!("OIDC callback received for authenticated user. Redirecting to {redirect_url}");
395404
request.into_response(build_redirect_response(redirect_url))
396405
}
@@ -425,8 +434,6 @@ async fn process_oidc_callback(
425434
) -> anyhow::Result<HttpResponse> {
426435
let http_client = get_http_client_from_appdata(request)?;
427436

428-
let state = get_state_from_cookie(request).context("Failed to read oidc state cookie")?;
429-
430437
let params = Query::<OidcCallbackParams>::from_query(query_string)
431438
.with_context(|| {
432439
format!(
@@ -435,22 +442,23 @@ async fn process_oidc_callback(
435442
})?
436443
.into_inner();
437444

438-
if state.csrf_token.secret() != params.state.secret() {
439-
log::debug!("CSRF token mismatch: expected {state:?}, got {params:?}");
440-
return Err(anyhow!("Invalid CSRF token: {}", params.state.secret()));
441-
}
445+
let mut redirect_url_cookie = get_redirect_url_cookie(request, &params.state)
446+
.with_context(|| "Failed to read redirect URL from cookie")?;
442447

443448
let client = oidc_state.get_client().await;
444449
log::debug!("Processing OIDC callback with params: {params:?}. Requesting token...");
445-
let token_response = exchange_code_for_token(&client, http_client, params).await?;
450+
let token_response = exchange_code_for_token(&client, http_client, params.clone()).await?;
446451
log::debug!("Received token response: {token_response:?}");
447452

448-
let redirect_target = validate_redirect_url(state.initial_url);
453+
let redirect_target = validate_redirect_url(redirect_url_cookie.value().to_string());
449454
log::info!("Redirecting to {redirect_target} after a successful login");
450455
let mut response = build_redirect_response(redirect_target);
451-
set_auth_cookie(&mut response, &token_response, oidc_state)
452-
.await
453-
.context("Failed to set auth cookie")?;
456+
set_auth_cookie(&mut response, &token_response).context("Failed to set auth cookie")?;
457+
458+
// Clean up the state-specific cookie after successful authentication
459+
redirect_url_cookie.set_path("/");
460+
response.add_removal_cookie(&redirect_url_cookie)?;
461+
454462
Ok(response)
455463
}
456464

@@ -469,21 +477,16 @@ async fn exchange_code_for_token(
469477
Ok(token_response)
470478
}
471479

472-
async fn set_auth_cookie(
480+
fn set_auth_cookie(
473481
response: &mut HttpResponse,
474482
token_response: &OidcTokenResponse,
475-
oidc_state: &OidcState,
476483
) -> anyhow::Result<()> {
477484
let access_token = token_response.access_token();
478485
log::trace!("Received access token: {}", access_token.secret());
479486
let id_token = token_response
480487
.id_token()
481488
.context("No ID token found in the token response. You may have specified an oauth2 provider that does not support OIDC.")?;
482489

483-
let claims_res = oidc_state.get_token_claims(id_token.clone(), None).await;
484-
let expiration = claims_res.context("Parsing ID token claims")?.expiration();
485-
let max_age_seconds = expiration.signed_duration_since(Utc::now()).num_seconds();
486-
487490
let id_token_str = id_token.to_string();
488491
log::trace!("Setting auth cookie: {SQLPAGE_AUTH_COOKIE_NAME}=\"{id_token_str}\"");
489492
let id_token_size_kb = id_token_str.len() / 1024;
@@ -496,7 +499,7 @@ async fn set_auth_cookie(
496499
let cookie = Cookie::build(SQLPAGE_AUTH_COOKIE_NAME, id_token_str)
497500
.secure(true)
498501
.http_only(true)
499-
.max_age(actix_web::cookie::time::Duration::seconds(max_age_seconds))
502+
.max_age(AUTH_COOKIE_EXPIRATION)
500503
.same_site(actix_web::cookie::SameSite::Lax)
501504
.path("/")
502505
.finish();
@@ -507,14 +510,15 @@ async fn set_auth_cookie(
507510

508511
async fn build_auth_provider_redirect_response(
509512
oidc_state: &OidcState,
510-
initial_url: String,
513+
initial_url: &str,
511514
) -> HttpResponse {
512515
let AuthUrl { url, params } = build_auth_url(oidc_state).await;
513-
let state = OidcLoginState::new(initial_url, params);
514-
let state_cookie = create_state_cookie(&state);
516+
let nonce_cookie = create_nonce_cookie(&params.nonce);
517+
let redirect_cookie = create_redirect_cookie(&params.csrf_token, initial_url);
515518
HttpResponse::TemporaryRedirect()
516-
.append_header(("Location", url.to_string()))
517-
.cookie(state_cookie)
519+
.append_header((header::LOCATION, url.to_string()))
520+
.cookie(nonce_cookie)
521+
.cookie(redirect_cookie)
518522
.body("Redirecting...")
519523
}
520524

@@ -536,9 +540,9 @@ async fn get_authenticated_user_info(
536540
let id_token = OidcToken::from_str(&cookie_value)
537541
.with_context(|| format!("Invalid SQLPage auth cookie: {cookie_value:?}"))?;
538542

539-
let state = get_state_from_cookie(request)?;
543+
let nonce = get_nonce_from_cookie(request)?;
540544
log::debug!("Verifying id token: {id_token:?}");
541-
let claims = oidc_state.get_token_claims(id_token, Some(&state)).await?;
545+
let claims = oidc_state.get_token_claims(id_token, &nonce).await?;
542546
log::debug!("The current user is: {claims:?}");
543547
Ok(Some(claims))
544548
}
@@ -689,7 +693,7 @@ fn make_oidc_client(
689693
Ok(client)
690694
}
691695

692-
#[derive(Debug, Deserialize)]
696+
#[derive(Debug, Deserialize, Clone)]
693697
struct OidcCallbackParams {
694698
code: String,
695699
state: CsrfToken,
@@ -739,15 +743,9 @@ fn hash_nonce(nonce: &Nonce) -> String {
739743
hash.to_string()
740744
}
741745

742-
fn check_nonce(
743-
id_token_nonce: Option<&Nonce>,
744-
login_state: Option<&OidcLoginState>,
745-
) -> Result<(), String> {
746-
let Some(state) = login_state else {
747-
return Ok(()); // No login state, no nonce to check
748-
};
746+
fn check_nonce(id_token_nonce: Option<&Nonce>, expected_nonce: &Nonce) -> Result<(), String> {
749747
match id_token_nonce {
750-
Some(id_token_nonce) => nonce_matches(id_token_nonce, &state.nonce),
748+
Some(id_token_nonce) => nonce_matches(id_token_nonce, expected_nonce),
751749
None => Err("No nonce found in the ID token".to_string()),
752750
}
753751
}
@@ -774,46 +772,42 @@ fn nonce_matches(id_token_nonce: &Nonce, state_nonce: &Nonce) -> Result<(), Stri
774772
Ok(())
775773
}
776774

777-
#[derive(Debug, Serialize, Deserialize)]
778-
struct OidcLoginState {
779-
/// The URL to redirect to after the login process is complete.
780-
#[serde(rename = "u")]
781-
initial_url: String,
782-
/// The CSRF token to use for the login process.
783-
#[serde(rename = "c")]
784-
csrf_token: CsrfToken,
785-
/// The source nonce to use for the login process. It must be checked against the hash
786-
/// stored in the ID token.
787-
#[serde(rename = "n")]
788-
nonce: Nonce,
789-
}
790-
791-
impl OidcLoginState {
792-
fn new(initial_url: String, auth_url: AuthUrlParams) -> Self {
793-
Self {
794-
initial_url,
795-
csrf_token: auth_url.csrf_token,
796-
nonce: auth_url.nonce,
797-
}
798-
}
775+
fn create_nonce_cookie(nonce: &Nonce) -> Cookie<'_> {
776+
Cookie::build(SQLPAGE_NONCE_COOKIE_NAME, nonce.secret())
777+
.secure(true)
778+
.http_only(true)
779+
.same_site(actix_web::cookie::SameSite::Lax)
780+
.max_age(AUTH_COOKIE_EXPIRATION)
781+
.path("/")
782+
.finish()
799783
}
800784

801-
fn create_state_cookie(login_state: &OidcLoginState) -> Cookie<'_> {
802-
let state_json = serde_json::to_string(login_state).unwrap();
803-
Cookie::build(SQLPAGE_STATE_COOKIE_NAME, state_json)
785+
fn create_redirect_cookie<'a>(csrf_token: &CsrfToken, initial_url: &'a str) -> Cookie<'a> {
786+
let cookie_name = SQLPAGE_REDIRECT_URL_COOKIE_PREFIX.to_owned() + csrf_token.secret();
787+
Cookie::build(cookie_name, initial_url)
804788
.secure(true)
805789
.http_only(true)
806790
.same_site(actix_web::cookie::SameSite::Lax)
807791
.path("/")
792+
.max_age(actix_web::cookie::time::Duration::minutes(10))
808793
.finish()
809794
}
810795

811-
fn get_state_from_cookie(request: &ServiceRequest) -> anyhow::Result<OidcLoginState> {
812-
let state_cookie = request.cookie(SQLPAGE_STATE_COOKIE_NAME).with_context(|| {
813-
format!("No {SQLPAGE_STATE_COOKIE_NAME} cookie found for {SQLPAGE_REDIRECT_URI}")
814-
})?;
815-
serde_json::from_str(state_cookie.value())
816-
.with_context(|| format!("Failed to parse OIDC state from cookie: {state_cookie}"))
796+
fn get_nonce_from_cookie(request: &ServiceRequest) -> anyhow::Result<Nonce> {
797+
let cookie = request
798+
.cookie(SQLPAGE_NONCE_COOKIE_NAME)
799+
.with_context(|| format!("No {SQLPAGE_NONCE_COOKIE_NAME} cookie found"))?;
800+
Ok(Nonce::new(cookie.value().to_string()))
801+
}
802+
803+
fn get_redirect_url_cookie(
804+
request: &ServiceRequest,
805+
csrf_token: &CsrfToken,
806+
) -> anyhow::Result<Cookie<'static>> {
807+
let cookie_name = SQLPAGE_REDIRECT_URL_COOKIE_PREFIX.to_owned() + csrf_token.secret();
808+
request
809+
.cookie(&cookie_name)
810+
.with_context(|| format!("No {cookie_name} cookie found"))
817811
}
818812

819813
/// Given an audience, verify if it is trusted. The `client_id` is always trusted, independently of this function.

0 commit comments

Comments
 (0)