Skip to content
This repository has been archived by the owner on Aug 21, 2024. It is now read-only.

Commit

Permalink
Use cookies for flash messages
Browse files Browse the repository at this point in the history
  • Loading branch information
0rzech committed Apr 4, 2024
1 parent 1946d56 commit f51daa0
Show file tree
Hide file tree
Showing 12 changed files with 239 additions and 106 deletions.
107 changes: 96 additions & 11 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 2 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,16 @@ argon2 = { version = "0.5.3", features = ["std"] }
askama = { version = "0.12.1", features = ["with-axum"], default-features = false }
askama_axum = { version = "0.4.0", default-features = false }
axum = "0.7.4"
axum-extra = { version = "0.9.3", features = ["cookie-signed"] }
base64 = "0.22.0"
config = "0.14.0"
hex = "0.4.3"
hmac = { version = "0.12.1", features = ["std"] }
htmlescape = "0.3.1"
once_cell = "1.19.0"
rand = "0.8.5"
regex = "1.10.3"
reqwest = { version = "0.11.24", features = ["json"], default-features = false }
reqwest = { version = "0.11.24", features = ["cookies", "json"], default-features = false }
secrecy = { version = "0.8.0", features = ["serde"] }
serde = { version = "1.0.196", features = ["derive"] }
serde-aux = { version = "4.4.0", default-features = false }
sha2 = "0.10.8"
sqlx = { version = "0.7.3", features = ["macros", "migrate", "postgres", "time", "runtime-tokio", "tls-native-tls", "uuid"], default-features = false }
thiserror = "1.0.58"
time = { version = "0.3.34", features = ["macros", "serde"] }
Expand All @@ -42,7 +39,6 @@ tracing-bunyan-formatter = "0.3.9"
tracing-log = "0.2.0"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
unicode-segmentation = "1.11.0"
urlencoding = "2.1.3"
uuid = { version = "1.7.0", features = ["v4"] }
validator = "0.16.1"

Expand Down
2 changes: 1 addition & 1 deletion configuration/base.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
application:
port: 8000
hmac_secret: long-and-very-secret-random-key-needed-to-verify-message-integrity
cookie_key: long-and-very-secret-random-key-needed-to-verify-message-integrity
database:
host: localhost
port: 5432
Expand Down
12 changes: 9 additions & 3 deletions src/app_state.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
use crate::email_client::EmailClient;
use axum::http::Uri;
use secrecy::Secret;
use axum::{extract::FromRef, http::Uri};
use axum_extra::extract::cookie::Key;
use sqlx::PgPool;

#[derive(Clone)]
pub struct AppState {
pub db_pool: PgPool,
pub email_client: EmailClient,
pub base_url: Uri,
pub hmac_secret: Secret<String>,
pub cookie_key: Key,
}

impl FromRef<AppState> for Key {
fn from_ref(state: &AppState) -> Self {
state.cookie_key.clone()
}
}
2 changes: 1 addition & 1 deletion src/configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pub struct ApplicationSettings {
#[serde(deserialize_with = "deserialize_number_from_string")]
pub port: u16,
pub base_url: String,
pub hmac_secret: Secret<String>,
pub cookie_key: Secret<String>,
}

#[derive(Deserialize)]
Expand Down
52 changes: 32 additions & 20 deletions src/routes/login/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,20 @@ use axum::{
response::{IntoResponse, Redirect, Response},
Form,
};
use hmac::{Hmac, Mac};
use secrecy::{ExposeSecret, Secret};
use axum_extra::extract::{
cookie::{Cookie, SameSite},
SignedCookieJar,
};
use secrecy::Secret;
use serde::Deserialize;
use urlencoding::Encoded;

#[tracing::instrument(
skip(app_state, form),
skip(app_state, form, jar),
fields(username = tracing::field::Empty, user_id = tracing::field::Empty)
)]
pub(super) async fn login(
State(app_state): State<AppState>,
jar: SignedCookieJar,
Form(form): Form<FormData>,
) -> Result<Redirect, LoginError> {
tracing::Span::current().record("username", &tracing::field::display(&form.username));
Expand All @@ -32,17 +35,7 @@ pub(super) async fn login(
)
.await
.map_err(|e| match e {
AuthError::InvalidCredentials(_) => {
let error = format!("error={}", Encoded::new(e.to_string()));
let tag = format!("tag={:x}", {
let secret: &[u8] = app_state.hmac_secret.expose_secret().as_bytes();
let mut mac = Hmac::<sha2::Sha256>::new_from_slice(secret).unwrap();
mac.update(error.as_bytes());
mac.finalize().into_bytes()
});

LoginError::AuthError(e.into(), Redirect::to(&format!("/login?{error}&{tag}")))
}
AuthError::InvalidCredentials(_) => LoginError::AuthErrorWithResponse(e.into(), jar),
AuthError::UnexpectedError(_) => LoginError::UnexpectedError(e.into()),
})?;

Expand All @@ -60,18 +53,37 @@ pub(super) struct FormData {
#[derive(Debug, thiserror::Error)]
pub(super) enum LoginError {
#[error("Authentication failed")]
AuthError(#[source] anyhow::Error, Redirect),
AuthErrorWithResponse(#[source] anyhow::Error, SignedCookieJar),
#[error("Authentication failed")]
AuthError(#[source] anyhow::Error),
#[error("Something went wrong")]
UnexpectedError(#[from] anyhow::Error),
}

impl IntoResponse for LoginError {
fn into_response(self) -> Response {
tracing::error!("{:#?}", self);

match self {
Self::AuthError(_, redirect) => redirect.into_response(),
Self::UnexpectedError(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
Self::AuthErrorWithResponse(e, jar) => {
let error = Self::AuthError(e);
tracing::error!("{:#?}", error);

let jar = jar.add(
Cookie::build(("_flash", error.to_string()))
.http_only(true)
.same_site(SameSite::Strict),
);
let redirect = Redirect::to("/login");

(jar, redirect).into_response()
}
Self::AuthError(_) => {
tracing::error!("{:#?}", self);
StatusCode::UNAUTHORIZED.into_response()
}
Self::UnexpectedError(_) => {
tracing::error!("{:#?}", self);
StatusCode::INTERNAL_SERVER_ERROR.into_response()
}
}
}
}
Loading

0 comments on commit f51daa0

Please sign in to comment.