cookie handling

This commit is contained in:
Hamcha 2023-07-01 15:02:40 +02:00
parent 50e85f7de9
commit b3380648bb
Signed by: hamcha
GPG key ID: 1669C533B8CF6D89
8 changed files with 119 additions and 39 deletions

41
Cargo.lock generated
View file

@ -238,11 +238,21 @@ dependencies = [
"js-sys",
"num-traits",
"serde",
"time",
"time 0.1.45",
"wasm-bindgen",
"winapi",
]
[[package]]
name = "cookie"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24"
dependencies = [
"time 0.3.22",
"version_check",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.4"
@ -696,6 +706,7 @@ dependencies = [
"axum",
"axum-macros",
"chrono",
"cookie",
"figment",
"serde",
"serde_json",
@ -703,6 +714,7 @@ dependencies = [
"tokio",
"tracing",
"tracing-subscriber",
"url",
"uuid",
]
@ -1402,6 +1414,33 @@ dependencies = [
"winapi",
]
[[package]]
name = "time"
version = "0.3.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd"
dependencies = [
"itoa",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
[[package]]
name = "time-macros"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b"
dependencies = [
"time-core",
]
[[package]]
name = "tinyvec"
version = "1.6.0"

View file

@ -6,6 +6,7 @@ edition = "2021"
[dependencies]
axum = "0.6"
axum-macros = "0.3"
cookie = "0.17"
tracing = "0.1"
tracing-subscriber = "0.3"
tokio = { version = "1.28", features = ["full"] }
@ -17,6 +18,7 @@ figment = { version = "0.10", features = ["toml", "env"] }
chrono = { version = "0.4", features = ["serde", "clock"] }
anyhow = "1.0"
argon2 = "0.5"
url = "2.4"
[profile.dev.package.sqlx-macros]
opt-level = 3

View file

@ -3,10 +3,7 @@ use argon2::{
password_hash::{rand_core::OsRng, SaltString},
Argon2, PasswordHash, PasswordHasher, PasswordVerifier,
};
use chrono::{
serde::{ts_seconds, ts_seconds_option},
DateTime, Duration, Utc,
};
use chrono::{Duration, NaiveDateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Pool, Postgres};
use uuid::Uuid;
@ -35,12 +32,9 @@ pub struct User {
pub roles: Vec<Uuid>,
/// Times
#[serde(with = "ts_seconds")]
pub created_at: DateTime<Utc>,
#[serde(with = "ts_seconds_option")]
pub modified_at: Option<DateTime<Utc>>,
#[serde(with = "ts_seconds_option")]
pub deleted_at: Option<DateTime<Utc>>,
pub created_at: NaiveDateTime,
pub modified_at: Option<NaiveDateTime>,
pub deleted_at: Option<NaiveDateTime>,
}
impl Default for User {
@ -79,7 +73,7 @@ impl User {
id: result.id,
name: username.to_owned(),
roles: roles.to_owned(),
created_at: result.created_at.and_utc(),
created_at: result.created_at,
..Default::default()
})
}
@ -113,23 +107,21 @@ pub struct Session {
pub secret: String,
/// Times
#[serde(with = "ts_seconds")]
pub created_at: DateTime<Utc>,
#[serde(with = "ts_seconds")]
pub expires_at: DateTime<Utc>,
pub created_at: NaiveDateTime,
pub expires_at: NaiveDateTime,
}
impl Session {
pub async fn create(pool: &Pool<Postgres>, user_id: Uuid, duration: Duration) -> Result<Self> {
let now = Utc::now();
let now = Utc::now().naive_utc();
let expires = now + duration;
let secret = random();
let result = sqlx::query!(
"INSERT INTO sessions (actor, secret, created_at, expires_at) VALUES ($1, $2, $3, $4) RETURNING id",
user_id,
secret,
now.naive_utc(),
expires.naive_utc()
now,
expires
)
.fetch_one(pool)
.await?;
@ -150,13 +142,13 @@ impl Session {
}
pub async fn refresh(self: Self, pool: &Pool<Postgres>, duration: Duration) -> Result<Self> {
let expires_at = Utc::now() + duration;
let expires_at = (Utc::now() + duration).naive_utc();
let secret = random();
sqlx::query!(
"UPDATE sessions SET secret = $1, expires_at = $2 WHERE id = $3 RETURNING id",
secret,
expires_at.naive_utc(),
expires_at,
self.id
)
.fetch_one(pool)

View file

@ -1,5 +1,3 @@
use std::error::Error;
use axum::{
http::StatusCode,
response::{IntoResponse, Response},

View file

@ -41,6 +41,7 @@ async fn main() -> Result<()> {
let app = Router::new()
.route("/auth/login", post(routes::auth::login))
.route("/me", get(routes::auth::me))
.route("/pages/:site/:slug", get(routes::content::page))
.route("/admin/bootstrap", post(routes::admin::bootstrap))
.with_state(shared_state);

View file

@ -1,7 +1,8 @@
use axum::extract::State;
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::Json;
use serde_json::{json, Value};
use serde_json::json;
use std::sync::Arc;
use crate::auth::{random, User};
@ -9,16 +10,17 @@ use crate::error::AppError;
use crate::roles::ROLE_SUPERADMIN;
use crate::state::AppState;
pub async fn bootstrap(State(state): State<Arc<AppState>>) -> Result<Json<Value>, AppError> {
pub async fn bootstrap(State(state): State<Arc<AppState>>) -> impl IntoResponse {
// Only allow this request if the user table is completely empty!
let empty = sqlx::query!(
"SELECT CASE WHEN EXISTS(SELECT 1 FROM users) THEN false ELSE true END AS empty;"
)
.map(|row| row.empty.unwrap_or(true))
.fetch_one(&state.database)
.await?;
.await
.map_err(AppError::from)?;
if empty {
if !empty {
return Err(AppError::ClientError {
status: StatusCode::BAD_REQUEST,
code: "already-setup".into(),
@ -35,7 +37,8 @@ pub async fn bootstrap(State(state): State<Arc<AppState>>) -> Result<Json<Value>
&password,
&[ROLE_SUPERADMIN].to_vec(),
)
.await?;
.await
.map_err(AppError::from)?;
Ok(Json(json!({"username": username, "password": password})))
}

View file

@ -1,7 +1,13 @@
use axum::{extract::State, http::StatusCode, Json};
use axum::{
extract::State,
http::{header::SET_COOKIE, StatusCode},
response::{IntoResponse, Response},
Json,
};
use chrono::Duration;
use cookie::Cookie;
use serde::Deserialize;
use serde_json::{json, Value};
use serde_json::json;
use std::sync::Arc;
use crate::{
@ -19,8 +25,10 @@ pub struct LoginRequest {
pub async fn login(
State(state): State<Arc<AppState>>,
Json(payload): Json<LoginRequest>,
) -> Result<Json<Value>, AppError> {
let user = User::find(&state.database, payload.username.as_str()).await?;
) -> impl IntoResponse {
let user = User::find(&state.database, payload.username.as_str())
.await
.map_err(AppError::from)?;
let invalid = || -> AppError {
AppError::ClientError {
@ -33,7 +41,7 @@ pub async fn login(
let user = user.ok_or_else(invalid)?;
let plaintext = user.password.ok_or_else(invalid)?;
if !verify(&payload.password, &plaintext)? {
if !verify(&payload.password, &plaintext).map_err(AppError::from)? {
return Err(invalid());
}
@ -42,9 +50,28 @@ pub async fn login(
user.id,
Duration::seconds(state.config.session_duration.into()),
)
.await?;
.await
.map_err(AppError::from)?;
Ok(Json(
json!({ "session_token": session.token(), "expires_at": session.expires_at }),
))
let token = session.token();
let mut response: Response =
Json(json!({ "session_token": token, "expires_at": session.expires_at })).into_response();
let secure = state.config.secure();
let cookie = Cookie::build("session", token)
.domain(state.config.domain())
.secure(secure)
.http_only(!secure)
.path("/")
.finish();
response
.headers_mut()
.insert(SET_COOKIE, cookie.to_string().parse()?);
Ok(response)
}
pub async fn me() -> String {
"test".into()
}

View file

@ -1,19 +1,37 @@
use serde::{Deserialize, Serialize};
use sqlx::{Pool, Postgres};
use url::Url;
#[derive(Deserialize, Serialize, Clone)]
pub struct Config {
pub bind: String,
pub database_url: String,
pub session_duration: i32, // in seconds
pub base_url: String,
}
impl Config {
pub fn secure(&self) -> bool {
Url::parse(&self.base_url)
.map(|x| x.scheme().to_owned())
.unwrap_or("http".to_owned())
== "https"
}
pub fn domain(&self) -> String {
Url::parse(&self.base_url)
.map(|x| x.domain().unwrap_or("localhost").to_owned())
.unwrap_or("localhost".to_owned())
}
}
impl Default for Config {
fn default() -> Self {
Config {
bind: "127.0.0.1:3000".to_owned(),
database_url: "postgres://artificiale:changeme@localhost/artificiale".to_owned(),
bind: "127.0.0.1:3000".into(),
database_url: "postgres://artificiale:changeme@localhost/artificiale".into(),
session_duration: 1440, // 24min
base_url: "http://localhost".into(),
}
}
}