Add machinery for seeding RNG for tests and start testing the auth flow
Signed-off-by: Olivier 'reivilibre <olivier@librepush.net>
This commit is contained in:
parent
6cd72b4177
commit
f2b0a64fb0
17
Cargo.lock
generated
17
Cargo.lock
generated
@ -1656,11 +1656,13 @@ dependencies = [
|
||||
"hornbeam",
|
||||
"insta",
|
||||
"josekit",
|
||||
"maplit",
|
||||
"metrics",
|
||||
"metrics-exporter-prometheus",
|
||||
"metrics-process",
|
||||
"pgtemp",
|
||||
"rand",
|
||||
"rand_xoshiro",
|
||||
"rstest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -1983,6 +1985,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "maplit"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.1.0"
|
||||
@ -2658,6 +2666,15 @@ dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_xoshiro"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa"
|
||||
dependencies = [
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "raw-cpuid"
|
||||
version = "10.7.0"
|
||||
|
@ -27,6 +27,7 @@ metrics = "0.21.1"
|
||||
metrics-exporter-prometheus = "0.12.1"
|
||||
metrics-process = "1.0.12"
|
||||
rand = "0.8.5"
|
||||
rand_xoshiro = "0.6.0"
|
||||
serde = { version = "1.0.188", features = ["derive"] }
|
||||
serde_json = "1.0.108"
|
||||
serde_urlencoded = "0.7.1"
|
||||
@ -44,5 +45,7 @@ tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
||||
assert_matches2 = "0.1.2"
|
||||
axum-test-helper = "0.3.0"
|
||||
insta = { version = "1.39.0", features = ["serde", "yaml"] }
|
||||
maplit = "1.0.2"
|
||||
pgtemp = "0.3.0"
|
||||
rand_xoshiro = "0.6.0"
|
||||
rstest = "0.21.0"
|
||||
|
@ -74,6 +74,9 @@
|
||||
# Test coverage. Vaguely useful but not definitive.
|
||||
pkgs.cargo-tarpaulin
|
||||
|
||||
# Snapshot testing
|
||||
pkgs.cargo-insta
|
||||
|
||||
pkgs.grass-sass
|
||||
pkgs.entr
|
||||
|
||||
|
@ -9,6 +9,7 @@ pub mod cli;
|
||||
pub mod config;
|
||||
pub mod passwords;
|
||||
pub mod store;
|
||||
pub mod utils;
|
||||
pub mod web;
|
||||
|
||||
#[cfg(test)]
|
||||
|
19
src/tests.rs
19
src/tests.rs
@ -4,11 +4,14 @@ use axum::Router;
|
||||
use confique::{Config, Partial};
|
||||
use josekit::jwk::alg::rsa::RsaKeyPair;
|
||||
use pgtemp::PgTempDB;
|
||||
use rand::SeedableRng;
|
||||
use rand_xoshiro::Xoshiro256StarStar;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{
|
||||
config::{Configuration, SecretConfig},
|
||||
store::IdCoopStore,
|
||||
utils::{Clock, RandGen},
|
||||
web::make_router,
|
||||
};
|
||||
|
||||
@ -23,6 +26,7 @@ const RSA_KEY_PAIR_PEM: &[u8] = include_bytes!("tests/keypair.pem");
|
||||
const RSA_PUBLIC_KEY_PEM: &[u8] = include_bytes!("tests/publickey.crt");
|
||||
|
||||
mod test_cli;
|
||||
mod test_oidc_auth_flow;
|
||||
|
||||
async fn basic_system() -> TestSystem {
|
||||
let temp_db = pgtemp::PgTempDBBuilder::new()
|
||||
@ -40,6 +44,7 @@ async fn basic_system() -> TestSystem {
|
||||
// Not useful, not actually used in the tests
|
||||
"bind": "127.0.0.1:1",
|
||||
"public_base_uri": "http://idcoop.example.com",
|
||||
"client_ip_source": "RightmostXForwardedFor",
|
||||
},
|
||||
"postgres": {
|
||||
"connect": "postgres://not-used-in-tests"
|
||||
@ -80,9 +85,17 @@ async fn basic_system() -> TestSystem {
|
||||
|
||||
let config = Arc::new(config);
|
||||
let store = Arc::new(store);
|
||||
let router = make_router(store.clone(), config.clone(), Arc::new(secrets))
|
||||
.await
|
||||
.expect("failed to make router");
|
||||
let clock = Clock::Fake();
|
||||
let randgen = RandGen(Xoshiro256StarStar::seed_from_u64(424242));
|
||||
let router = make_router(
|
||||
store.clone(),
|
||||
config.clone(),
|
||||
Arc::new(secrets),
|
||||
clock,
|
||||
randgen,
|
||||
)
|
||||
.await
|
||||
.expect("failed to make router");
|
||||
|
||||
TestSystem {
|
||||
database: temp_db,
|
||||
|
@ -0,0 +1,9 @@
|
||||
---
|
||||
source: src/tests/test_oidc_auth_flow.rs
|
||||
expression: "(headers, text)"
|
||||
---
|
||||
- content-length: "238"
|
||||
content-type: text/html; charset=utf-8
|
||||
set-cookie: __Host-SessionlessXsrf=HL4qRFKUlBqkrPTvAQ6z-w; HttpOnly; SameSite=Strict; Secure; Path=/; Max-Age=43200000
|
||||
x-frame-options: DENY
|
||||
- "<form method='POST'>UN<input type='text' name='username'> PW<input type='password' name='password'> <input type='hidden' name='xsrf' value='HL4qRFKUlBqkrPTvAQ6z-w'><button type='submit'>click here to login</button> (temporary form)</form>"
|
@ -0,0 +1,10 @@
|
||||
---
|
||||
source: src/tests/test_oidc_auth_flow.rs
|
||||
expression: "(headers, text)"
|
||||
---
|
||||
- content-length: "55"
|
||||
content-type: text/plain; charset=utf-8
|
||||
location: "/oidc/auth?scope=openid&client_id=aclient&response_type=code&state=wombat&redirect_uri=http:%2F%2Faclient.example.com%2Fredirect&code_challenge=challenging&code_challenge_method=S256&nonce=noncey"
|
||||
set-cookie: __Host-LoginSession=HL4qRFKUlBqkrPTvAQ6z-xpYf2uo9sbO68miVnnz7KE; HttpOnly; SameSite=Strict; Secure; Path=/; Max-Age=43200000
|
||||
x-frame-options: DENY
|
||||
- Logged in. Redirecting you back to what you were doing.
|
@ -0,0 +1,9 @@
|
||||
---
|
||||
source: src/tests/test_oidc_auth_flow.rs
|
||||
expression: "(headers, text)"
|
||||
---
|
||||
- content-length: "238"
|
||||
content-type: text/html; charset=utf-8
|
||||
set-cookie: __Host-SessionlessXsrf=HL4qRFKUlBqkrPTvAQ6z-w; HttpOnly; SameSite=Strict; Secure; Path=/; Max-Age=43200000
|
||||
x-frame-options: DENY
|
||||
- "<form method='POST'>UN<input type='text' name='username'> PW<input type='password' name='password'> <input type='hidden' name='xsrf' value='HL4qRFKUlBqkrPTvAQ6z-w'><button type='submit'>click here to login</button> (temporary form)</form>"
|
91
src/tests/test_oidc_auth_flow.rs
Normal file
91
src/tests/test_oidc_auth_flow.rs
Normal file
@ -0,0 +1,91 @@
|
||||
//! Tests the OpenID Connect auth flow
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use axum::http::StatusCode;
|
||||
use axum_test_helper::{TestClient, TestResponse};
|
||||
use insta::assert_yaml_snapshot;
|
||||
|
||||
use maplit::btreemap;
|
||||
|
||||
use crate::{passwords::create_password_hash, store::CreateUser, tests::basic_system};
|
||||
|
||||
async fn dump_resp_text(
|
||||
req_name: &str,
|
||||
resp: TestResponse,
|
||||
) -> (StatusCode, BTreeMap<String, String>, String) {
|
||||
let status = resp.status();
|
||||
// convert headers to a simple B-Tree map so they can be serialised in snapshots
|
||||
// easily
|
||||
let mut headers: BTreeMap<String, String> = resp
|
||||
.headers()
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k.unwrap().to_string(), v.to_str().unwrap().to_owned()))
|
||||
.collect();
|
||||
// Remove date because it's not stable across tests!
|
||||
headers.remove("date");
|
||||
let text = resp.text().await;
|
||||
eprintln!("=== Response for {req_name} ===");
|
||||
eprintln!("Status: {status:?}");
|
||||
eprintln!("Headers: {headers:#?}");
|
||||
eprintln!("Body: {text:?}");
|
||||
eprintln!("=== End of response ===");
|
||||
(status, headers, text)
|
||||
}
|
||||
|
||||
/// Tests the full flow...
|
||||
#[tokio::test]
|
||||
async fn test_todo() {
|
||||
let sys = basic_system().await;
|
||||
|
||||
let pwhash = create_password_hash("secret", &sys.config.password_hashing).unwrap();
|
||||
let _: () = sys
|
||||
.store
|
||||
.txn(|mut txn| {
|
||||
Box::pin(async move {
|
||||
txn.create_user(CreateUser {
|
||||
user_login_name: "robert".to_owned(),
|
||||
password_hash: Some(pwhash),
|
||||
locked: false,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let client = TestClient::new(sys.web);
|
||||
|
||||
// 1. /auth request
|
||||
const LOGIN_URL: &str = "/login?then=%2Foidc%2Fauth%3Fscope%3Dopenid%26client_id%3Daclient%26response_type%3Dcode%26state%3Dwombat%26redirect_uri%3Dhttp%3A%252F%252Faclient.example.com%252Fredirect%26code_challenge%3Dchallenging%26code_challenge_method%3DS256%26nonce%3Dnoncey";
|
||||
let resp = client.get("/oidc/auth?scope=openid&client_id=aclient&response_type=code&state=wombat&redirect_uri=http:%2F%2Faclient.example.com%2Fredirect&code_challenge=challenging&code_challenge_method=S256&nonce=noncey").send().await;
|
||||
let (status, headers, _text) = dump_resp_text("1. /auth request", resp).await;
|
||||
assert_eq!(status, 302);
|
||||
assert_eq!(headers.get("location").unwrap(), LOGIN_URL);
|
||||
|
||||
// 2. /login request
|
||||
let resp = client.get(LOGIN_URL).send().await;
|
||||
let (status, headers, text) = dump_resp_text("2. /login request", resp).await;
|
||||
assert_eq!(status, 200);
|
||||
assert_yaml_snapshot!("2/login", (headers, text));
|
||||
|
||||
// 3. /login request with credentials
|
||||
let resp = client
|
||||
.post(LOGIN_URL)
|
||||
.form(&btreemap! {
|
||||
"username" => "robert",
|
||||
"password" => "secret",
|
||||
"xsrf" => "HL4qRFKUlBqkrPTvAQ6z-w",
|
||||
})
|
||||
.header("Cookie", "__Host-SessionlessXsrf=HL4qRFKUlBqkrPTvAQ6z-w")
|
||||
// /login is rate-limited by IP source and needs an IP
|
||||
.header("X-Forwarded-For", "0.0.0.0")
|
||||
.send()
|
||||
.await;
|
||||
let (status, headers, text) = dump_resp_text("3. /login request with credentials", resp).await;
|
||||
assert_eq!(status, 302);
|
||||
assert_yaml_snapshot!("3/login", (headers, text));
|
||||
}
|
66
src/utils.rs
Normal file
66
src/utils.rs
Normal file
@ -0,0 +1,66 @@
|
||||
//! Miscellaneous utilities
|
||||
|
||||
#[cfg(not(test))]
|
||||
mod real_utils {
|
||||
use rand::{thread_rng, RngCore};
|
||||
|
||||
/// A source of random numbers that can be faked for tests.
|
||||
#[derive(Clone)]
|
||||
pub struct RandGen;
|
||||
|
||||
impl RngCore for RandGen {
|
||||
fn next_u32(&mut self) -> u32 {
|
||||
thread_rng().next_u32()
|
||||
}
|
||||
|
||||
fn next_u64(&mut self) -> u64 {
|
||||
thread_rng().next_u64()
|
||||
}
|
||||
|
||||
fn fill_bytes(&mut self, dest: &mut [u8]) {
|
||||
thread_rng().fill_bytes(dest)
|
||||
}
|
||||
|
||||
fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand::Error> {
|
||||
thread_rng().try_fill_bytes(dest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_utils {
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use rand_xoshiro::Xoshiro256StarStar;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RandGen(pub Xoshiro256StarStar);
|
||||
|
||||
impl Deref for RandGen {
|
||||
type Target = Xoshiro256StarStar;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
impl DerefMut for RandGen {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
pub use self::real_utils::RandGen;
|
||||
|
||||
#[cfg(test)]
|
||||
pub use self::test_utils::RandGen;
|
||||
|
||||
/// A source of time that can be faked for tests.
|
||||
#[derive(Clone)]
|
||||
pub enum Clock {
|
||||
/// Use real time
|
||||
Real,
|
||||
/// Fake time for use in tests
|
||||
Fake(),
|
||||
}
|
16
src/web.rs
16
src/web.rs
@ -24,16 +24,16 @@ use axum::{
|
||||
routing::{get, post},
|
||||
Extension, Router,
|
||||
};
|
||||
use eyre::Context;
|
||||
use governor::{clock::QuantaClock, state::keyed::DashMapStateStore, RateLimiter};
|
||||
use hornbeam::{initialise_template_manager, make_template_manager};
|
||||
use tower_cookies::CookieManagerLayer;
|
||||
use tower_http::{cors::CorsLayer, set_header::SetResponseHeaderLayer, trace::TraceLayer};
|
||||
use tracing::{error, info};
|
||||
use tracing::error;
|
||||
|
||||
use crate::{
|
||||
config::{Configuration, RatelimiterConfig, RatelimitsConfig, SecretConfig},
|
||||
store::IdCoopStore,
|
||||
utils::{Clock, RandGen},
|
||||
web::{
|
||||
login::{get_login, post_login, PasswordHashInflightLimiter},
|
||||
oauth_openid::{
|
||||
@ -61,6 +61,8 @@ pub(crate) async fn make_router(
|
||||
store: Arc<IdCoopStore>,
|
||||
config: Arc<Configuration>,
|
||||
secrets: Arc<SecretConfig>,
|
||||
clock: Clock,
|
||||
randgen: RandGen,
|
||||
) -> eyre::Result<Router> {
|
||||
initialise_template_manager!(TEMPLATING);
|
||||
|
||||
@ -123,20 +125,26 @@ pub(crate) async fn make_router(
|
||||
.layer(Extension(Arc::new(PasswordHashInflightLimiter::new(1))))
|
||||
.layer(client_ip_source.into_extension())
|
||||
.layer(Extension(Arc::new(ratelimiters)))
|
||||
.layer(Extension(VolatileCodeStore::default()));
|
||||
.layer(Extension(VolatileCodeStore::default()))
|
||||
.layer(Extension(clock))
|
||||
.layer(Extension(randgen));
|
||||
|
||||
Ok(router)
|
||||
}
|
||||
|
||||
/// Serves, on the bind address specified, the HTTP service
|
||||
/// including a user interface and any OAuth, OpenID Connect and custom APIs.
|
||||
#[cfg(not(test))]
|
||||
pub async fn serve(
|
||||
bind: SocketAddr,
|
||||
store: Arc<IdCoopStore>,
|
||||
config: Arc<Configuration>,
|
||||
secrets: Arc<SecretConfig>,
|
||||
) -> eyre::Result<()> {
|
||||
let router = make_router(store, config, secrets).await?;
|
||||
use eyre::Context;
|
||||
use tracing::info;
|
||||
|
||||
let router = make_router(store, config, secrets, Clock::Real, RandGen).await?;
|
||||
|
||||
info!("Listening on {bind:?}");
|
||||
axum::Server::try_bind(&bind)
|
||||
|
@ -9,8 +9,8 @@ use std::{
|
||||
use async_trait::async_trait;
|
||||
use axum::{
|
||||
extract::{FromRequestParts, Query},
|
||||
headers::Cookie,
|
||||
http::{request::Parts, uri::PathAndQuery, HeaderValue, StatusCode},
|
||||
headers::Cookie as CookieHeader,
|
||||
http::{request::Parts, uri::PathAndQuery, StatusCode},
|
||||
response::{Html, IntoResponse, Response},
|
||||
Extension, Form, TypedHeader,
|
||||
};
|
||||
@ -25,13 +25,14 @@ use rand::{thread_rng, Rng};
|
||||
use serde::Deserialize;
|
||||
use sqlx::types::Uuid;
|
||||
use tokio::sync::Semaphore;
|
||||
use tower_cookies::Cookies;
|
||||
use tower_cookies::{Cookie, Cookies};
|
||||
use tracing::error;
|
||||
|
||||
use crate::{
|
||||
config::{Configuration, PasswordHashingConfig},
|
||||
passwords::{check_hash, create_password_hash},
|
||||
store::IdCoopStore,
|
||||
utils::RandGen,
|
||||
};
|
||||
|
||||
use super::{sessionless_xsrf, Ratelimiters, WebResult};
|
||||
@ -203,17 +204,15 @@ where
|
||||
type Rejection = (StatusCode, &'static str);
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||
let Ok(cookies) = TypedHeader::<Cookie>::from_request_parts(parts, state).await else {
|
||||
let Ok(cookies) = TypedHeader::<CookieHeader>::from_request_parts(parts, state).await
|
||||
else {
|
||||
return Err((StatusCode::UNAUTHORIZED, "No login session."));
|
||||
};
|
||||
let Some(cookie_val) = cookies.get("__Host-LoginSession").map(str::to_owned) else {
|
||||
return Err((StatusCode::UNAUTHORIZED, "No login session."));
|
||||
};
|
||||
let Ok(login_session_token) = BASE64_URL_SAFE_NO_PAD.decode(&cookie_val) else {
|
||||
return Err((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"Invalid login session token."
|
||||
));
|
||||
return Err((StatusCode::UNAUTHORIZED, "Invalid login session token."));
|
||||
};
|
||||
if login_session_token.len() != LOGIN_SESSION_TOKEN_BYTES {
|
||||
return Err((StatusCode::UNAUTHORIZED, "Invalid login session token."));
|
||||
@ -268,11 +267,12 @@ pub async fn get_login(
|
||||
current_session: Option<LoginSession>,
|
||||
Query(query): Query<LoginQuery>,
|
||||
cookies: Cookies,
|
||||
Extension(mut randgen): Extension<RandGen>,
|
||||
) -> Response {
|
||||
match current_session {
|
||||
Some(_session) => make_post_login_redirect(query.then),
|
||||
None => {
|
||||
let xsrf_token = sessionless_xsrf::get_token(&cookies);
|
||||
let xsrf_token = sessionless_xsrf::get_token(&cookies, &mut randgen);
|
||||
Html(format!("<form method='POST'>UN<input type='text' name='username'> PW<input type='password' name='password'> <input type='hidden' name='xsrf' value='{}'><button type='submit'>click here to login</button> (temporary form)</form>", xsrf_token)).into_response()
|
||||
}
|
||||
}
|
||||
@ -335,6 +335,7 @@ pub async fn post_login(
|
||||
Extension(phil): Extension<Arc<PasswordHashInflightLimiter>>,
|
||||
SecureClientIp(src_ip): SecureClientIp,
|
||||
Extension(ratelimiters): Extension<Arc<Ratelimiters>>,
|
||||
Extension(mut randgen): Extension<RandGen>,
|
||||
Form(form): Form<PostLoginForm>,
|
||||
) -> WebResult<Response> {
|
||||
ratelimiters.housekeeping();
|
||||
@ -344,7 +345,7 @@ pub async fn post_login(
|
||||
.await;
|
||||
if !sessionless_xsrf::check_token(&cookies, &form.xsrf) {
|
||||
// Invalid XSRF token: try again
|
||||
return Ok(get_login(None, Query(query), cookies).await);
|
||||
return Ok(get_login(None, Query(query), cookies, Extension(randgen)).await);
|
||||
}
|
||||
|
||||
// retrieve user details
|
||||
@ -400,11 +401,11 @@ pub async fn post_login(
|
||||
};
|
||||
|
||||
// Generate a login session token and store the hash in our database
|
||||
let login_session_token = thread_rng().gen::<[u8; LOGIN_SESSION_TOKEN_BYTES]>();
|
||||
let login_session_token = randgen.gen::<[u8; LOGIN_SESSION_TOKEN_BYTES]>();
|
||||
let login_session_token_b64 = BASE64_URL_SAFE_NO_PAD.encode(login_session_token);
|
||||
let login_session_token_hash: [u8; LOGIN_SESSION_TOKEN_HASH_BYTES] =
|
||||
Blake2s256::digest(login_session_token).into();
|
||||
let xsrf_secret = thread_rng().gen::<[u8; LOGIN_SESSION_XSRF_SECRET_BYTES]>();
|
||||
let xsrf_secret = randgen.gen::<[u8; LOGIN_SESSION_XSRF_SECRET_BYTES]>();
|
||||
|
||||
// store session in the database
|
||||
store
|
||||
@ -417,20 +418,16 @@ pub async fn post_login(
|
||||
.await
|
||||
.context("failed to store session in database")?;
|
||||
|
||||
let expiry_date = chrono::Utc::now() + chrono::Duration::days(500);
|
||||
let expiry_date_rfc1123 = expiry_date.format("%a, %d %b %Y %H:%M:%S GMT");
|
||||
Ok((
|
||||
[(
|
||||
"Set-Cookie",
|
||||
HeaderValue::from_str(&format!(
|
||||
"__Host-LoginSession={}; Path=/; HttpOnly; SameSite=Strict; Secure; Expires={}",
|
||||
login_session_token_b64, expiry_date_rfc1123
|
||||
))
|
||||
.expect("no reason we should fail to make a cookie"),
|
||||
)],
|
||||
make_post_login_redirect(query.then),
|
||||
)
|
||||
.into_response())
|
||||
cookies.add(
|
||||
Cookie::build("__Host-LoginSession", login_session_token_b64.clone())
|
||||
.path("/")
|
||||
.http_only(true)
|
||||
.secure(true)
|
||||
.same_site(tower_cookies::cookie::SameSite::Strict)
|
||||
.max_age(time::Duration::days(500))
|
||||
.finish(),
|
||||
);
|
||||
Ok(make_post_login_redirect(query.then))
|
||||
}
|
||||
|
||||
/// Make a redirect for once the user has logged in.
|
||||
|
@ -7,20 +7,22 @@
|
||||
//! Even on older browsers, that type of attack is rare, so this 'naïve' scheme is fine for the purpose (rather than having to do anything complicated).
|
||||
|
||||
use base64::{prelude::BASE64_URL_SAFE_NO_PAD, Engine};
|
||||
use rand::{thread_rng, Rng};
|
||||
use rand::Rng;
|
||||
use subtle::ConstantTimeEq;
|
||||
use time::Duration;
|
||||
use tower_cookies::{Cookie, Cookies};
|
||||
|
||||
use crate::utils::RandGen;
|
||||
|
||||
/// Name of the cookie for the sessionless cross-site request forgery prevention cookie.
|
||||
pub const COOKIE_NAME: &str = "__Host-SessionlessXsrf";
|
||||
|
||||
/// Gets the Sessionless XSRF token to put into a form request
|
||||
pub fn get_token(cookies: &Cookies) -> String {
|
||||
pub fn get_token(cookies: &Cookies, randgen: &mut RandGen) -> String {
|
||||
if let Some(xsrf_cookie) = cookies.get(COOKIE_NAME) {
|
||||
xsrf_cookie.value().to_owned()
|
||||
} else {
|
||||
let new_token = thread_rng().gen::<[u8; 16]>();
|
||||
let new_token = randgen.gen::<[u8; 16]>();
|
||||
let new_token_b64 = BASE64_URL_SAFE_NO_PAD.encode(new_token);
|
||||
cookies.add(
|
||||
Cookie::build(COOKIE_NAME, new_token_b64.clone())
|
||||
@ -37,7 +39,9 @@ pub fn get_token(cookies: &Cookies) -> String {
|
||||
|
||||
/// Checks a Sessionless XSRF token obtained from a form request
|
||||
pub fn check_token(cookies: &Cookies, token: &str) -> bool {
|
||||
let Some(xsrf_token) = cookies.get(COOKIE_NAME) else { return false };
|
||||
let Some(xsrf_token) = cookies.get(COOKIE_NAME) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
bool::from(xsrf_token.value().as_bytes().ct_eq(token.as_bytes()))
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user