CSRF -> XSRF (I use the XSRF abbreviation more myself)
This commit is contained in:
parent
5a5dfe36b9
commit
adfc1b1cf6
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "\n SELECT user_name, user_id, login_session_id, csrf_secret\n FROM login_sessions INNER JOIN users USING (user_id)\n WHERE login_session_token_hash = $1\n ",
|
"query": "\n SELECT user_name, user_id, login_session_id, xsrf_secret\n FROM login_sessions INNER JOIN users USING (user_id)\n WHERE login_session_token_hash = $1\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@ -20,7 +20,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 3,
|
"ordinal": 3,
|
||||||
"name": "csrf_secret",
|
"name": "xsrf_secret",
|
||||||
"type_info": "Bytea"
|
"type_info": "Bytea"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -36,5 +36,5 @@
|
|||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "d6b173d90cd227a65bfc4a47ed3ba7b19650ed4d3dc44c1519775a6d0c9026b7"
|
"hash": "125d60c302bfc35fa7edc71b4c23c1a4fd81060df92388ccbfd43dd8c5771031"
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "INSERT INTO login_sessions (login_session_token_hash, user_id, started_at_utc, csrf_secret)\n VALUES ($1, $2, NOW(), $3)",
|
"query": "INSERT INTO login_sessions (login_session_token_hash, user_id, started_at_utc, xsrf_secret)\n VALUES ($1, $2, NOW(), $3)",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
@ -12,5 +12,5 @@
|
|||||||
},
|
},
|
||||||
"nullable": []
|
"nullable": []
|
||||||
},
|
},
|
||||||
"hash": "2633347de5bc78129d60926bbeab5571eddce6f76d80db323622db9648d918c3"
|
"hash": "cfc3e3102493b5b385bcab8eb8d4ea73d38cfb32cdf5c34b6cc76738669490fd"
|
||||||
}
|
}
|
@ -5,7 +5,7 @@ CREATE TABLE login_sessions (
|
|||||||
login_session_token_hash BYTEA NOT NULL UNIQUE,
|
login_session_token_hash BYTEA NOT NULL UNIQUE,
|
||||||
user_id UUID NOT NULL REFERENCES users(user_id),
|
user_id UUID NOT NULL REFERENCES users(user_id),
|
||||||
started_at_utc TIMESTAMP NOT NULL,
|
started_at_utc TIMESTAMP NOT NULL,
|
||||||
csrf_secret BYTEA NOT NULL
|
xsrf_secret BYTEA NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX ON login_sessions (user_id);
|
CREATE INDEX ON login_sessions (user_id);
|
||||||
@ -20,4 +20,4 @@ COMMENT ON COLUMN login_sessions.user_id IS 'ID of the user that this login sess
|
|||||||
|
|
||||||
COMMENT ON COLUMN login_sessions.started_at_utc IS 'Timestamp in UTC when the session was started.';
|
COMMENT ON COLUMN login_sessions.started_at_utc IS 'Timestamp in UTC when the session was started.';
|
||||||
|
|
||||||
COMMENT ON COLUMN login_sessions.csrf_secret IS 'Key for a Blake2sMac256 which is used to prevent Cross-Site Request Forgery.';
|
COMMENT ON COLUMN login_sessions.xsrf_secret IS 'Key for a Blake2sMac256 which is used to prevent Cross-Site Request Forgery.';
|
||||||
|
16
src/store.rs
16
src/store.rs
@ -8,7 +8,7 @@ use sqlx::{types::Uuid, Connection, PgPool, Postgres, Transaction};
|
|||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
use crate::web::login::{
|
use crate::web::login::{
|
||||||
LoginSession, LOGIN_SESSION_CSRF_SECRET_BYTES, LOGIN_SESSION_TOKEN_HASH_BYTES,
|
LoginSession, LOGIN_SESSION_TOKEN_HASH_BYTES, LOGIN_SESSION_XSRF_SECRET_BYTES,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Postgres-backed storage for IdCoop
|
/// Postgres-backed storage for IdCoop
|
||||||
@ -239,12 +239,12 @@ impl<'a, 'txn> IdCoopStoreTxn<'a, 'txn> {
|
|||||||
&mut self,
|
&mut self,
|
||||||
login_session_token_hash: &[u8; LOGIN_SESSION_TOKEN_HASH_BYTES],
|
login_session_token_hash: &[u8; LOGIN_SESSION_TOKEN_HASH_BYTES],
|
||||||
user_id: Uuid,
|
user_id: Uuid,
|
||||||
csrf_secret: &[u8; LOGIN_SESSION_CSRF_SECRET_BYTES],
|
xsrf_secret: &[u8; LOGIN_SESSION_XSRF_SECRET_BYTES],
|
||||||
) -> eyre::Result<()> {
|
) -> eyre::Result<()> {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"INSERT INTO login_sessions (login_session_token_hash, user_id, started_at_utc, csrf_secret)
|
"INSERT INTO login_sessions (login_session_token_hash, user_id, started_at_utc, xsrf_secret)
|
||||||
VALUES ($1, $2, NOW(), $3)",
|
VALUES ($1, $2, NOW(), $3)",
|
||||||
login_session_token_hash, user_id, csrf_secret
|
login_session_token_hash, user_id, xsrf_secret
|
||||||
)
|
)
|
||||||
.execute(&mut **self.txn)
|
.execute(&mut **self.txn)
|
||||||
.await
|
.await
|
||||||
@ -258,7 +258,7 @@ impl<'a, 'txn> IdCoopStoreTxn<'a, 'txn> {
|
|||||||
) -> eyre::Result<Option<LoginSession>> {
|
) -> eyre::Result<Option<LoginSession>> {
|
||||||
let row_opt = sqlx::query!(
|
let row_opt = sqlx::query!(
|
||||||
"
|
"
|
||||||
SELECT user_name, user_id, login_session_id, csrf_secret
|
SELECT user_name, user_id, login_session_id, xsrf_secret
|
||||||
FROM login_sessions INNER JOIN users USING (user_id)
|
FROM login_sessions INNER JOIN users USING (user_id)
|
||||||
WHERE login_session_token_hash = $1
|
WHERE login_session_token_hash = $1
|
||||||
",
|
",
|
||||||
@ -274,10 +274,10 @@ impl<'a, 'txn> IdCoopStoreTxn<'a, 'txn> {
|
|||||||
user_name: row.user_name,
|
user_name: row.user_name,
|
||||||
user_id: row.user_id,
|
user_id: row.user_id,
|
||||||
login_session_id: row.login_session_id,
|
login_session_id: row.login_session_id,
|
||||||
csrf_secret: row
|
xsrf_secret: row
|
||||||
.csrf_secret
|
.xsrf_secret
|
||||||
.try_into()
|
.try_into()
|
||||||
.map_err(|_| eyre!("cannot retrieve login session: has invalid CSRF token"))?,
|
.map_err(|_| eyre!("cannot retrieve login session: has invalid XSRF token"))?,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,28 +32,28 @@ pub const LOGIN_SESSION_TOKEN_BYTES: usize = 32;
|
|||||||
/// Size of a Blake2s hash of the login token (which is what we store in the database)
|
/// Size of a Blake2s hash of the login token (which is what we store in the database)
|
||||||
pub const LOGIN_SESSION_TOKEN_HASH_BYTES: usize = 32;
|
pub const LOGIN_SESSION_TOKEN_HASH_BYTES: usize = 32;
|
||||||
|
|
||||||
/// Size of the CSRF secret in bytes; this is a Blake2sMac256 salt size.
|
/// Size of the XSRF secret in bytes; this is a Blake2sMac256 salt size.
|
||||||
/// TODO the Blake2sMac256 also has a 'personal' item which might give us more bytes to play with and
|
/// TODO the Blake2sMac256 also has a 'personal' item which might give us more bytes to play with and
|
||||||
/// perhaps we should be using that too.
|
/// perhaps we should be using that too.
|
||||||
pub const LOGIN_SESSION_CSRF_SECRET_BYTES: usize = 8;
|
pub const LOGIN_SESSION_XSRF_SECRET_BYTES: usize = 8;
|
||||||
|
|
||||||
pub struct LoginSession {
|
pub struct LoginSession {
|
||||||
pub user_name: String,
|
pub user_name: String,
|
||||||
pub user_id: Uuid,
|
pub user_id: Uuid,
|
||||||
pub login_session_id: i32,
|
pub login_session_id: i32,
|
||||||
pub csrf_secret: [u8; LOGIN_SESSION_CSRF_SECRET_BYTES],
|
pub xsrf_secret: [u8; LOGIN_SESSION_XSRF_SECRET_BYTES],
|
||||||
}
|
}
|
||||||
|
|
||||||
/// CSRF token expiry time is 1 week.
|
/// XSRF token expiry time is 1 week.
|
||||||
pub const CSRF_TOKEN_EXPIRY_TIME: Duration = Duration::milliseconds(1000 * 86400 * 7);
|
pub const XSRF_TOKEN_EXPIRY_TIME: Duration = Duration::milliseconds(1000 * 86400 * 7);
|
||||||
|
|
||||||
impl LoginSession {
|
impl LoginSession {
|
||||||
/// Generates a CSRF token which is bound to this session and expires 1 week in the future.
|
/// Generates a XSRF token which is bound to this session and expires 1 week in the future.
|
||||||
pub fn generate_csrf_token(&self, now: DateTime<Utc>) -> eyre::Result<String> {
|
pub fn generate_xsrf_token(&self, now: DateTime<Utc>) -> eyre::Result<String> {
|
||||||
let now_timestamp = now.timestamp();
|
let now_timestamp = now.timestamp();
|
||||||
let now_8bytes = now_timestamp.to_be_bytes();
|
let now_8bytes = now_timestamp.to_be_bytes();
|
||||||
|
|
||||||
let mac_tag_bytes = Blake2sMac256::new_with_salt_and_personal(&self.csrf_secret, &[], &[])?
|
let mac_tag_bytes = Blake2sMac256::new_with_salt_and_personal(&self.xsrf_secret, &[], &[])?
|
||||||
.chain_update(&now_8bytes)
|
.chain_update(&now_8bytes)
|
||||||
.finalize()
|
.finalize()
|
||||||
.into_bytes();
|
.into_bytes();
|
||||||
@ -62,34 +62,34 @@ impl LoginSession {
|
|||||||
Ok(format!("{now_timestamp}.{mac_b64}"))
|
Ok(format!("{now_timestamp}.{mac_b64}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validates a CSRF token to check it is bound to this session and hasn't expired (is less than a week old).
|
/// Validates a XSRF token to check it is bound to this session and hasn't expired (is less than a week old).
|
||||||
pub fn validate_csrf_token(&self, token: &str, now: DateTime<Utc>) -> eyre::Result<()> {
|
pub fn validate_xsrf_token(&self, token: &str, now: DateTime<Utc>) -> eyre::Result<()> {
|
||||||
let (timestamp_str, mac_b64) = token
|
let (timestamp_str, mac_b64) = token
|
||||||
.split_once('.')
|
.split_once('.')
|
||||||
.context("CSRF token in wrong format")?;
|
.context("XSRF token in wrong format")?;
|
||||||
let timestamp: i64 = timestamp_str
|
let timestamp: i64 = timestamp_str
|
||||||
.parse()
|
.parse()
|
||||||
.context("timestamp in CSRF token is wrong")?;
|
.context("timestamp in XSRF token is wrong")?;
|
||||||
let mac_tag_bytes: Vec<u8> = BASE64_URL_SAFE_NO_PAD
|
let mac_tag_bytes: Vec<u8> = BASE64_URL_SAFE_NO_PAD
|
||||||
.decode(mac_b64)
|
.decode(mac_b64)
|
||||||
.context("failed to b64decode the MAC in CSRF token")?;
|
.context("failed to b64decode the MAC in XSRF token")?;
|
||||||
|
|
||||||
let timestamp_8bytes = timestamp.to_be_bytes();
|
let timestamp_8bytes = timestamp.to_be_bytes();
|
||||||
|
|
||||||
Blake2sMac256::new_with_salt_and_personal(&self.csrf_secret, &[], &[])?
|
Blake2sMac256::new_with_salt_and_personal(&self.xsrf_secret, &[], &[])?
|
||||||
.chain_update(×tamp_8bytes)
|
.chain_update(×tamp_8bytes)
|
||||||
.verify_slice(&mac_tag_bytes)
|
.verify_slice(&mac_tag_bytes)
|
||||||
.context("bad MAC in CSRF token")?;
|
.context("bad MAC in XSRF token")?;
|
||||||
|
|
||||||
// At this point, the MAC is correct. All that's left is to check that the timestamp isn't too old.
|
// At this point, the MAC is correct. All that's left is to check that the timestamp isn't too old.
|
||||||
|
|
||||||
if now.signed_duration_since(
|
if now.signed_duration_since(
|
||||||
Utc.timestamp_opt(timestamp, 0)
|
Utc.timestamp_opt(timestamp, 0)
|
||||||
.earliest()
|
.earliest()
|
||||||
.context("CSRF timestamp not valid")?,
|
.context("XSRF timestamp not valid")?,
|
||||||
) > CSRF_TOKEN_EXPIRY_TIME
|
) > XSRF_TOKEN_EXPIRY_TIME
|
||||||
{
|
{
|
||||||
bail!("CSRF token expired.");
|
bail!("XSRF token expired.");
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -162,8 +162,8 @@ pub async fn get_login(
|
|||||||
match current_session {
|
match current_session {
|
||||||
Some(_session) => make_post_login_redirect(query.then),
|
Some(_session) => make_post_login_redirect(query.then),
|
||||||
None => {
|
None => {
|
||||||
let csrf_token = sessionless_xsrf::get_token(&cookies);
|
let xsrf_token = sessionless_xsrf::get_token(&cookies);
|
||||||
Html(format!("<form method='POST'>UN<input type='text' name='username'> PW<input type='password' name='password'> <input type='hidden' name='csrf' value='{}'><button type='submit'>click here to login</button> (temporary form)</form>", csrf_token)).into_response()
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -172,7 +172,7 @@ pub async fn get_login(
|
|||||||
pub struct PostLoginForm {
|
pub struct PostLoginForm {
|
||||||
username: String,
|
username: String,
|
||||||
password: String,
|
password: String,
|
||||||
csrf: String,
|
xsrf: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dummy_password_hash(
|
fn dummy_password_hash(
|
||||||
@ -197,8 +197,8 @@ pub async fn post_login(
|
|||||||
Extension(config): Extension<Arc<Configuration>>,
|
Extension(config): Extension<Arc<Configuration>>,
|
||||||
Form(form): Form<PostLoginForm>,
|
Form(form): Form<PostLoginForm>,
|
||||||
) -> WebResult<Response> {
|
) -> WebResult<Response> {
|
||||||
if !sessionless_xsrf::check_token(&cookies, &form.csrf) {
|
if !sessionless_xsrf::check_token(&cookies, &form.xsrf) {
|
||||||
// Invalid CSRF token: try again
|
// Invalid XSRF token: try again
|
||||||
return Ok(get_login(None, Query(query), cookies).await);
|
return Ok(get_login(None, Query(query), cookies).await);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -240,13 +240,13 @@ pub async fn post_login(
|
|||||||
let login_session_token_b64 = BASE64_URL_SAFE_NO_PAD.encode(login_session_token);
|
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] =
|
let login_session_token_hash: [u8; LOGIN_SESSION_TOKEN_HASH_BYTES] =
|
||||||
Blake2s256::digest(&login_session_token).into();
|
Blake2s256::digest(&login_session_token).into();
|
||||||
let csrf_secret = thread_rng().gen::<[u8; LOGIN_SESSION_CSRF_SECRET_BYTES]>();
|
let xsrf_secret = thread_rng().gen::<[u8; LOGIN_SESSION_XSRF_SECRET_BYTES]>();
|
||||||
|
|
||||||
// store session in the database
|
// store session in the database
|
||||||
store
|
store
|
||||||
.txn(|mut txn| {
|
.txn(|mut txn| {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
txn.create_login_session(&login_session_token_hash, user.user_id, &csrf_secret)
|
txn.create_login_session(&login_session_token_hash, user.user_id, &xsrf_secret)
|
||||||
.await
|
.await
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -106,7 +106,7 @@ pub async fn oidc_authorisation(
|
|||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct PostConsentForm {
|
pub struct PostConsentForm {
|
||||||
action: String,
|
action: String,
|
||||||
csrf: String,
|
xsrf: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn post_oidc_authorisation_consent(
|
pub async fn post_oidc_authorisation_consent(
|
||||||
@ -128,7 +128,7 @@ pub async fn post_oidc_authorisation_consent(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if login_session
|
if login_session
|
||||||
.validate_csrf_token(&form.csrf, Utc::now())
|
.validate_xsrf_token(&form.xsrf, Utc::now())
|
||||||
.is_err()
|
.is_err()
|
||||||
{
|
{
|
||||||
// XSRF token is not valid, so show the consent form again...
|
// XSRF token is not valid, so show the consent form again...
|
||||||
@ -212,12 +212,12 @@ async fn show_consent_page(
|
|||||||
client_config: &OidcClientConfiguration,
|
client_config: &OidcClientConfiguration,
|
||||||
_config: &Configuration,
|
_config: &Configuration,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
let csrf_token = login_session
|
let xsrf_token = login_session
|
||||||
.generate_csrf_token(Utc::now())
|
.generate_xsrf_token(Utc::now())
|
||||||
.expect("must be able to create a CSRF token");
|
.expect("must be able to create a XSRF token");
|
||||||
Html(format!(
|
Html(format!(
|
||||||
"hi <u>{}</u>, consent to <u>{}</u>? <form method='POST'><input type='hidden' name='csrf' value='{}'><button type='submit' name='action' value='accept'>Accept</button> <button type='submit' name='action' value='deny'>Deny</button></form>",
|
"hi <u>{}</u>, consent to <u>{}</u>? <form method='POST'><input type='hidden' name='xsrf' value='{}'><button type='submit' name='action' value='accept'>Accept</button> <button type='submit' name='action' value='deny'>Deny</button></form>",
|
||||||
login_session.user_name, client_config.name, csrf_token
|
login_session.user_name, client_config.name, xsrf_token
|
||||||
))
|
))
|
||||||
.into_response()
|
.into_response()
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user