Skip to content

Commit 92ba450

Browse files
committed
implement csrf token
1 parent 55bf296 commit 92ba450

File tree

1 file changed

+94
-32
lines changed

1 file changed

+94
-32
lines changed

src/webserver/oidc.rs

Lines changed: 94 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1-
use std::{future::Future, pin::Pin, str::FromStr, sync::Arc};
1+
use std::{
2+
future::Future,
3+
hash::{DefaultHasher, Hash, Hasher},
4+
pin::Pin,
5+
str::FromStr,
6+
sync::Arc,
7+
};
28

39
use crate::{app_config::AppConfig, AppState};
410
use actix_web::{
11+
cookie::Cookie,
512
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
613
middleware::Condition,
714
web::{self, Query},
@@ -10,11 +17,13 @@ use actix_web::{
1017
use anyhow::{anyhow, Context};
1118
use awc::Client;
1219
use openidconnect::{
13-
core::{CoreAuthDisplay, CoreAuthenticationFlow, CoreGenderClaim, CoreIdToken},
20+
core::{CoreAuthenticationFlow, CoreGenderClaim, CoreIdToken},
21+
url::Url,
1422
AsyncHttpClient, CsrfToken, EmptyAdditionalClaims, EndpointMaybeSet, EndpointNotSet,
15-
EndpointSet, IdToken, IdTokenClaims, IssuerUrl, Nonce, OAuth2TokenResponse, RedirectUrl, Scope,
23+
EndpointSet, IdTokenClaims, IssuerUrl, Nonce, OAuth2TokenResponse, RedirectUrl, Scope,
1624
TokenResponse,
1725
};
26+
use password_hash::{rand_core::OsRng, SaltString};
1827
use serde::{Deserialize, Serialize};
1928

2029
use super::http_client::make_http_client;
@@ -208,18 +217,9 @@ impl<S> OidcService<S> {
208217

209218
log::debug!("Redirecting to OIDC provider");
210219

211-
let auth_url = build_auth_url(
212-
&self.oidc_client,
213-
&self.config.scopes,
214-
request.path().to_string(),
215-
);
216-
Box::pin(async move {
217-
let state_cookie = create_state_cookie(&request);
218-
let mut response = build_redirect_response(auth_url);
219-
220-
response.add_cookie(&state_cookie)?;
221-
Ok(request.into_response(response))
222-
})
220+
let response =
221+
build_auth_provider_redirect_response(&self.oidc_client, &self.config, &request);
222+
Box::pin(async move { Ok(request.into_response(response)) })
223223
}
224224

225225
fn handle_oidc_callback(
@@ -236,12 +236,9 @@ impl<S> OidcService<S> {
236236
Ok(response) => Ok(request.into_response(response)),
237237
Err(e) => {
238238
log::error!("Failed to process OIDC callback with params {query_string}: {e}");
239-
let auth_url = build_auth_url(
240-
&oidc_client,
241-
&oidc_config.scopes,
242-
request.path().to_string(),
243-
);
244-
Ok(request.into_response(build_redirect_response(auth_url)))
239+
let resp =
240+
build_auth_provider_redirect_response(&oidc_client, &oidc_config, &request);
241+
Ok(request.into_response(resp))
245242
}
246243
}
247244
})
@@ -301,6 +298,12 @@ async fn process_oidc_callback(
301298
)
302299
})?
303300
.into_inner();
301+
302+
if state.csrf_token.secret() != params.state.secret() {
303+
log::debug!("CSRF token mismatch: expected {state:?}, got {params:?}");
304+
return Err(anyhow!("Invalid CSRF token: {}", params.state.secret()));
305+
}
306+
304307
log::debug!("Processing OIDC callback with params: {params:?}. Requesting token...");
305308
let token_response = exchange_code_for_token(oidc_client, http_client, params).await?;
306309
log::debug!("Received token response: {token_response:?}");
@@ -337,7 +340,7 @@ fn set_auth_cookie(
337340

338341
let id_token_str = id_token.to_string();
339342
log::trace!("Setting auth cookie: {SQLPAGE_AUTH_COOKIE_NAME}=\"{id_token_str}\"");
340-
let cookie = actix_web::cookie::Cookie::build(SQLPAGE_AUTH_COOKIE_NAME, id_token_str)
343+
let cookie = Cookie::build(SQLPAGE_AUTH_COOKIE_NAME, id_token_str)
341344
.secure(true)
342345
.http_only(true)
343346
.same_site(actix_web::cookie::SameSite::Lax)
@@ -348,6 +351,19 @@ fn set_auth_cookie(
348351
Ok(())
349352
}
350353

354+
fn build_auth_provider_redirect_response(
355+
oidc_client: &OidcClient,
356+
oidc_config: &Arc<OidcConfig>,
357+
request: &ServiceRequest,
358+
) -> HttpResponse {
359+
let AuthUrl { url, params } = build_auth_url(oidc_client, &oidc_config.scopes);
360+
let state_cookie = create_state_cookie(request, params);
361+
HttpResponse::TemporaryRedirect()
362+
.append_header(("Location", url.to_string()))
363+
.cookie(state_cookie)
364+
.body("Redirecting...")
365+
}
366+
351367
fn build_redirect_response(target_url: String) -> HttpResponse {
352368
HttpResponse::TemporaryRedirect()
353369
.append_header(("Location", target_url))
@@ -496,33 +512,79 @@ fn make_oidc_client(
496512
#[derive(Debug, Deserialize)]
497513
struct OidcCallbackParams {
498514
code: String,
499-
state: String,
515+
state: CsrfToken,
500516
}
501517

502-
fn build_auth_url(oidc_client: &OidcClient, scopes: &[Scope], initial_url: String) -> String {
503-
let (auth_url, csrf_token, nonce) = oidc_client
518+
struct AuthUrl {
519+
url: Url,
520+
params: AuthUrlParams,
521+
}
522+
523+
struct AuthUrlParams {
524+
csrf_token: CsrfToken,
525+
nonce: Nonce,
526+
}
527+
528+
fn build_auth_url(oidc_client: &OidcClient, scopes: &[Scope]) -> AuthUrl {
529+
let nonce_source = Nonce::new_random();
530+
let hashed_nonce = Nonce::new(hash_nonce(&nonce_source));
531+
let (url, csrf_token, _nonce) = oidc_client
504532
.authorize_url(
505533
CoreAuthenticationFlow::AuthorizationCode,
506534
CsrfToken::new_random,
507-
Nonce::new_random,
535+
|| hashed_nonce,
508536
)
509537
.add_scopes(scopes.iter().cloned())
510538
.url();
511-
auth_url.to_string()
539+
AuthUrl {
540+
url,
541+
params: AuthUrlParams {
542+
csrf_token,
543+
nonce: nonce_source,
544+
},
545+
}
512546
}
513547

514548
#[derive(Debug, Serialize, Deserialize)]
515549
struct OidcLoginState {
550+
/// The URL to redirect to after the login process is complete.
516551
#[serde(rename = "u")]
517552
initial_url: String,
553+
/// The CSRF token to use for the login process.
554+
#[serde(rename = "c")]
555+
csrf_token: CsrfToken,
556+
/// The source nonce to use for the login process. It must be checked against the hash
557+
/// stored in the ID token.
558+
#[serde(rename = "n")]
559+
nonce: Nonce,
518560
}
519561

520-
fn create_state_cookie(request: &ServiceRequest) -> actix_web::cookie::Cookie {
521-
let state = OidcLoginState {
522-
initial_url: request.path().to_string(),
523-
};
562+
fn hash_nonce(nonce: &Nonce) -> String {
563+
use argon2::password_hash::{rand_core::OsRng, PasswordHasher, SaltString};
564+
let salt = SaltString::generate(&mut OsRng);
565+
// low-cost parameters
566+
let params = argon2::Params::new(8, 1, 1, None).expect("bug: invalid Argon2 parameters");
567+
let argon2 = argon2::Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params);
568+
let hash = argon2
569+
.hash_password(nonce.secret().as_bytes(), &salt)
570+
.expect("bug: failed to hash nonce");
571+
hash.to_string()
572+
}
573+
574+
impl OidcLoginState {
575+
fn new(request: &ServiceRequest, auth_url: AuthUrlParams) -> Self {
576+
Self {
577+
initial_url: request.path().to_string(),
578+
csrf_token: auth_url.csrf_token,
579+
nonce: auth_url.nonce,
580+
}
581+
}
582+
}
583+
584+
fn create_state_cookie(request: &ServiceRequest, auth_url: AuthUrlParams) -> Cookie {
585+
let state = OidcLoginState::new(request, auth_url);
524586
let state_json = serde_json::to_string(&state).unwrap();
525-
actix_web::cookie::Cookie::build(SQLPAGE_STATE_COOKIE_NAME, state_json)
587+
Cookie::build(SQLPAGE_STATE_COOKIE_NAME, state_json)
526588
.secure(true)
527589
.http_only(true)
528590
.same_site(actix_web::cookie::SameSite::Lax)

0 commit comments

Comments
 (0)