Skip to content

Commit 8c4a300

Browse files
cursoragentlovasoa
andcommitted
Refactor OIDC state management to use separate nonce and redirect cookies
Co-authored-by: contact <[email protected]>
1 parent b6f81b9 commit 8c4a300

File tree

1 file changed

+68
-114
lines changed

1 file changed

+68
-114
lines changed

src/webserver/oidc.rs

Lines changed: 68 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -210,11 +210,11 @@ impl OidcState {
210210
async fn get_token_claims(
211211
&self,
212212
id_token: OidcToken,
213-
state: Option<&OidcLoginState>,
213+
expected_nonce: Option<&Nonce>,
214214
) -> anyhow::Result<OidcClaims> {
215215
let client = &self.get_client().await;
216216
let verifier = self.config.create_id_token_verifier(client);
217-
let nonce_verifier = |nonce: Option<&Nonce>| check_nonce(nonce, state);
217+
let nonce_verifier = |nonce: Option<&Nonce>| check_nonce(nonce, expected_nonce);
218218
let claims: OidcClaims = id_token
219219
.into_claims(&verifier, nonce_verifier)
220220
.map_err(|e| anyhow::anyhow!("Could not verify the ID token: {}", e))?;
@@ -379,11 +379,8 @@ async fn handle_oidc_callback(oidc_state: &OidcState, request: ServiceRequest) -
379379
// Try to get redirect URL from query params if available
380380
let redirect_url =
381381
if let Ok(params) = Query::<OidcCallbackParams>::from_query(query_string) {
382-
if let Ok(state) = get_state_from_cookies(&request, &params.state) {
383-
state.initial_url
384-
} else {
385-
"/".to_string()
386-
}
382+
get_redirect_url_from_cookie(&request, &params.state)
383+
.unwrap_or_else(|_| "/".to_string())
387384
} else {
388385
"/".to_string()
389386
};
@@ -398,16 +395,13 @@ async fn handle_oidc_callback(oidc_state: &OidcState, request: ServiceRequest) -
398395
/// When an user has already authenticated (potentially in another tab), we ignore the callback and redirect to the initial URL.
399396
fn handle_authenticated_oidc_callback(request: ServiceRequest) -> ServiceResponse {
400397
// Try to get redirect URL from query params if available
401-
let redirect_url =
402-
if let Ok(params) = Query::<OidcCallbackParams>::from_query(request.query_string()) {
403-
if let Ok(state) = get_state_from_cookies(&request, &params.state) {
404-
state.initial_url
405-
} else {
406-
"/".to_string()
407-
}
408-
} else {
409-
"/".to_string()
410-
};
398+
let redirect_url = if let Ok(params) =
399+
Query::<OidcCallbackParams>::from_query(request.query_string())
400+
{
401+
get_redirect_url_from_cookie(&request, &params.state).unwrap_or_else(|_| "/".to_string())
402+
} else {
403+
"/".to_string()
404+
};
411405
log::debug!("OIDC callback received for authenticated user. Redirecting to {redirect_url}");
412406
request.into_response(build_redirect_response(redirect_url))
413407
}
@@ -450,34 +444,23 @@ async fn process_oidc_callback(
450444
})?
451445
.into_inner();
452446

453-
let state = get_state_from_cookies(request, &params.state)
454-
.context("Failed to read oidc state cookies")?;
447+
let redirect_url = get_redirect_url_from_cookie(request, &params.state)
448+
.context("Failed to read redirect URL from cookie")?;
455449

456450
let client = oidc_state.get_client().await;
457451
log::debug!("Processing OIDC callback with params: {params:?}. Requesting token...");
458-
let token_response = exchange_code_for_token(&client, http_client, params).await?;
452+
let token_response = exchange_code_for_token(&client, http_client, params.clone()).await?;
459453
log::debug!("Received token response: {token_response:?}");
460454

461-
let redirect_target = validate_redirect_url(state.initial_url);
455+
let redirect_target = validate_redirect_url(redirect_url);
462456
log::info!("Redirecting to {redirect_target} after a successful login");
463457
let mut response = build_redirect_response(redirect_target);
464458
set_auth_cookie(&mut response, &token_response, oidc_state)
465459
.await
466460
.context("Failed to set auth cookie")?;
467461

468462
// Clean up the state-specific cookie after successful authentication
469-
let state_cookie_name = format!(
470-
"{}{}",
471-
SQLPAGE_STATE_COOKIE_PREFIX,
472-
state.csrf_token.secret()
473-
);
474-
let cleanup_cookie = Cookie::build(state_cookie_name, "")
475-
.secure(true)
476-
.http_only(true)
477-
.same_site(actix_web::cookie::SameSite::Lax)
478-
.path("/")
479-
.max_age(actix_web::cookie::time::Duration::seconds(0)) // Expire immediately
480-
.finish();
463+
let cleanup_cookie = create_cleanup_cookie(&params.state);
481464
response.add_cookie(&cleanup_cookie).unwrap();
482465

483466
Ok(response)
@@ -539,8 +522,8 @@ async fn build_auth_provider_redirect_response(
539522
initial_url: String,
540523
) -> HttpResponse {
541524
let AuthUrl { url, params } = build_auth_url(oidc_state).await;
542-
let state = OidcLoginState::new(initial_url, params);
543-
let (nonce_cookie, redirect_cookie) = create_state_cookies(&state);
525+
let nonce_cookie = create_nonce_cookie(&params.nonce);
526+
let redirect_cookie = create_redirect_cookie(&params.csrf_token, &initial_url);
544527
HttpResponse::TemporaryRedirect()
545528
.append_header(("Location", url.to_string()))
546529
.cookie(nonce_cookie)
@@ -566,10 +549,10 @@ async fn get_authenticated_user_info(
566549
let id_token = OidcToken::from_str(&cookie_value)
567550
.with_context(|| format!("Invalid SQLPage auth cookie: {cookie_value:?}"))?;
568551

569-
// Try to get state from cookies if this is a callback request
570-
let state = if request.path() == SQLPAGE_REDIRECT_URI {
571-
if let Ok(params) = Query::<OidcCallbackParams>::from_query(request.query_string()) {
572-
get_state_from_cookies(request, &params.state).ok()
552+
// Try to get nonce from cookies if this is a callback request
553+
let nonce = if request.path() == SQLPAGE_REDIRECT_URI {
554+
if let Ok(_params) = Query::<OidcCallbackParams>::from_query(request.query_string()) {
555+
get_nonce_from_cookie(request).ok()
573556
} else {
574557
None
575558
}
@@ -579,7 +562,7 @@ async fn get_authenticated_user_info(
579562

580563
log::debug!("Verifying id token: {id_token:?}");
581564
let claims = oidc_state
582-
.get_token_claims(id_token, state.as_ref())
565+
.get_token_claims(id_token, nonce.as_ref())
583566
.await?;
584567
log::debug!("The current user is: {claims:?}");
585568
Ok(Some(claims))
@@ -731,7 +714,7 @@ fn make_oidc_client(
731714
Ok(client)
732715
}
733716

734-
#[derive(Debug, Deserialize)]
717+
#[derive(Debug, Deserialize, Clone)]
735718
struct OidcCallbackParams {
736719
code: String,
737720
state: CsrfToken,
@@ -783,13 +766,13 @@ fn hash_nonce(nonce: &Nonce) -> String {
783766

784767
fn check_nonce(
785768
id_token_nonce: Option<&Nonce>,
786-
login_state: Option<&OidcLoginState>,
769+
expected_nonce: Option<&Nonce>,
787770
) -> Result<(), String> {
788-
let Some(state) = login_state else {
789-
return Ok(()); // No login state, no nonce to check
771+
let Some(expected) = expected_nonce else {
772+
return Ok(()); // No expected nonce, no validation needed
790773
};
791774
match id_token_nonce {
792-
Some(id_token_nonce) => nonce_matches(id_token_nonce, &state.nonce),
775+
Some(id_token_nonce) => nonce_matches(id_token_nonce, expected),
793776
None => Err("No nonce found in the ID token".to_string()),
794777
}
795778
}
@@ -816,107 +799,78 @@ fn nonce_matches(id_token_nonce: &Nonce, state_nonce: &Nonce) -> Result<(), Stri
816799
Ok(())
817800
}
818801

819-
#[derive(Debug, Serialize, Deserialize)]
820-
struct OidcLoginState {
821-
/// The URL to redirect to after the login process is complete.
822-
#[serde(rename = "u")]
823-
initial_url: String,
824-
/// The CSRF token to use for the login process.
825-
#[serde(rename = "c")]
826-
csrf_token: CsrfToken,
827-
/// The source nonce to use for the login process. It must be checked against the hash
828-
/// stored in the ID token.
829-
#[serde(rename = "n")]
830-
nonce: Nonce,
831-
}
832-
833802
#[derive(Debug, Serialize, Deserialize)]
834803
struct OidcNonceState {
835-
/// The source nonce to use for the login process. It must be checked against the hash
836-
/// stored in the ID token.
837804
#[serde(rename = "n")]
838805
nonce: Nonce,
839806
}
840807

841808
#[derive(Debug, Serialize, Deserialize)]
842809
struct OidcRedirectState {
843-
/// The URL to redirect to after the login process is complete.
844810
#[serde(rename = "u")]
845811
initial_url: String,
846812
}
847813

848-
impl OidcLoginState {
849-
fn new(initial_url: String, auth_url: AuthUrlParams) -> Self {
850-
Self {
851-
initial_url,
852-
csrf_token: auth_url.csrf_token,
853-
nonce: auth_url.nonce,
854-
}
855-
}
856-
}
857-
858-
fn create_state_cookies(login_state: &OidcLoginState) -> (Cookie<'_>, Cookie<'_>) {
859-
// Create nonce cookie (longer-lived for security)
814+
fn create_nonce_cookie(nonce: &Nonce) -> Cookie<'_> {
860815
let nonce_state = OidcNonceState {
861-
nonce: login_state.nonce.clone(),
816+
nonce: nonce.clone(),
862817
};
863818
let nonce_json = serde_json::to_string(&nonce_state).unwrap();
864-
let nonce_cookie = Cookie::build(SQLPAGE_NONCE_COOKIE_NAME, nonce_json)
819+
Cookie::build(SQLPAGE_NONCE_COOKIE_NAME, nonce_json)
865820
.secure(true)
866821
.http_only(true)
867822
.same_site(actix_web::cookie::SameSite::Lax)
868823
.path("/")
869-
.max_age(actix_web::cookie::time::Duration::minutes(10)) // 10 minutes should be enough for login flow
870-
.finish();
824+
.max_age(actix_web::cookie::time::Duration::minutes(10))
825+
.finish()
826+
}
871827

872-
// Create state-specific redirect cookie (short-lived)
828+
fn create_redirect_cookie(csrf_token: &CsrfToken, initial_url: &str) -> Cookie<'static> {
873829
let redirect_state = OidcRedirectState {
874-
initial_url: login_state.initial_url.clone(),
830+
initial_url: initial_url.to_string(),
875831
};
876832
let redirect_json = serde_json::to_string(&redirect_state).unwrap();
877-
let state_cookie_name = format!(
878-
"{}{}",
879-
SQLPAGE_STATE_COOKIE_PREFIX,
880-
login_state.csrf_token.secret()
881-
);
882-
let redirect_cookie = Cookie::build(state_cookie_name, redirect_json)
833+
let cookie_name = format!("{}{}", SQLPAGE_STATE_COOKIE_PREFIX, csrf_token.secret());
834+
Cookie::build(cookie_name, redirect_json)
883835
.secure(true)
884836
.http_only(true)
885837
.same_site(actix_web::cookie::SameSite::Lax)
886838
.path("/")
887-
.max_age(actix_web::cookie::time::Duration::minutes(5)) // Short-lived, only needed during login flow
888-
.finish();
839+
.max_age(actix_web::cookie::time::Duration::minutes(5))
840+
.finish()
841+
}
889842

890-
(nonce_cookie, redirect_cookie)
843+
fn create_cleanup_cookie(csrf_token: &CsrfToken) -> Cookie<'_> {
844+
let cookie_name = format!("{}{}", SQLPAGE_STATE_COOKIE_PREFIX, csrf_token.secret());
845+
Cookie::build(cookie_name, "")
846+
.secure(true)
847+
.http_only(true)
848+
.same_site(actix_web::cookie::SameSite::Lax)
849+
.path("/")
850+
.max_age(actix_web::cookie::time::Duration::seconds(0))
851+
.finish()
852+
}
853+
854+
fn get_nonce_from_cookie(request: &ServiceRequest) -> anyhow::Result<Nonce> {
855+
let cookie = request
856+
.cookie(SQLPAGE_NONCE_COOKIE_NAME)
857+
.with_context(|| format!("No {SQLPAGE_NONCE_COOKIE_NAME} cookie found"))?;
858+
let nonce_state: OidcNonceState = serde_json::from_str(cookie.value())
859+
.with_context(|| format!("Failed to parse nonce from cookie: {cookie}"))?;
860+
Ok(nonce_state.nonce)
891861
}
892862

893-
fn get_state_from_cookies(
863+
fn get_redirect_url_from_cookie(
894864
request: &ServiceRequest,
895865
csrf_token: &CsrfToken,
896-
) -> anyhow::Result<OidcLoginState> {
897-
// Get nonce from the nonce cookie
898-
let nonce_cookie = request.cookie(SQLPAGE_NONCE_COOKIE_NAME).with_context(|| {
899-
format!("No {SQLPAGE_NONCE_COOKIE_NAME} cookie found for {SQLPAGE_REDIRECT_URI}")
900-
})?;
901-
let nonce_state: OidcNonceState = serde_json::from_str(nonce_cookie.value())
902-
.with_context(|| format!("Failed to parse OIDC nonce from cookie: {nonce_cookie}"))?;
903-
904-
// Get redirect URL from the state-specific cookie
905-
let state_cookie_name = format!("{}{}", SQLPAGE_STATE_COOKIE_PREFIX, csrf_token.secret());
906-
let redirect_cookie = request.cookie(&state_cookie_name).with_context(|| {
907-
format!("No {state_cookie_name} cookie found for {SQLPAGE_REDIRECT_URI}")
908-
})?;
909-
let redirect_state: OidcRedirectState = serde_json::from_str(redirect_cookie.value())
910-
.with_context(|| {
911-
format!("Failed to parse OIDC redirect state from cookie: {redirect_cookie}")
912-
})?;
913-
914-
// Reconstruct the full login state
915-
Ok(OidcLoginState {
916-
initial_url: redirect_state.initial_url,
917-
csrf_token: csrf_token.clone(),
918-
nonce: nonce_state.nonce,
919-
})
866+
) -> anyhow::Result<String> {
867+
let cookie_name = format!("{}{}", SQLPAGE_STATE_COOKIE_PREFIX, csrf_token.secret());
868+
let cookie = request
869+
.cookie(&cookie_name)
870+
.with_context(|| format!("No {cookie_name} cookie found"))?;
871+
let redirect_state: OidcRedirectState = serde_json::from_str(cookie.value())
872+
.with_context(|| format!("Failed to parse redirect state from cookie: {cookie}"))?;
873+
Ok(redirect_state.initial_url)
920874
}
921875

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

0 commit comments

Comments
 (0)