diff options
| author | Owen Jacobson <owen@grimoire.ca> | 2024-09-04 01:25:31 -0400 |
|---|---|---|
| committer | Owen Jacobson <owen@grimoire.ca> | 2024-09-04 01:25:54 -0400 |
| commit | 072dfa9a0bae5b7e9ea1caa97f6a90bd576a5d95 (patch) | |
| tree | 3194c56bbf1b9729d07198973815c0cb88a9e5c6 | |
| parent | 2965a788cfcf4a0386cb8832e0d96491bf54c1d3 (diff) | |
Expire sessions after 90 days.
| -rw-r--r-- | .sqlx/query-011ebe4604fb41aeec4330f5b66862611e6db9fd855fe12046a5c0576ca53d62.json | 12 | ||||
| -rw-r--r-- | src/cli.rs | 8 | ||||
| -rw-r--r-- | src/clock.rs | 51 | ||||
| -rw-r--r-- | src/lib.rs | 1 | ||||
| -rw-r--r-- | src/login/extract/login.rs | 3 | ||||
| -rw-r--r-- | src/login/repo/tokens.rs | 18 | ||||
| -rw-r--r-- | src/login/routes.rs | 6 |
7 files changed, 92 insertions, 7 deletions
diff --git a/.sqlx/query-011ebe4604fb41aeec4330f5b66862611e6db9fd855fe12046a5c0576ca53d62.json b/.sqlx/query-011ebe4604fb41aeec4330f5b66862611e6db9fd855fe12046a5c0576ca53d62.json new file mode 100644 index 0000000..51462ff --- /dev/null +++ b/.sqlx/query-011ebe4604fb41aeec4330f5b66862611e6db9fd855fe12046a5c0576ca53d62.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n delete\n from token\n where issued_at < $1\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "011ebe4604fb41aeec4330f5b66862611e6db9fd855fe12046a5c0576ca53d62" +} @@ -1,12 +1,12 @@ use std::io; use std::str::FromStr; -use axum::Router; +use axum::{middleware, Router}; use clap::Parser; use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions}; use tokio::net; -use crate::{error::BoxedError, index, login}; +use crate::{clock, error::BoxedError, index, login}; pub type Result<T> = std::result::Result<T, BoxedError>; @@ -28,7 +28,9 @@ impl Args { sqlx::migrate!().run(&pool).await?; - let app = routers().with_state(pool); + let app = routers() + .route_layer(middleware::from_fn(clock::middleware)) + .with_state(pool); let listener = self.listener().await?; let started_msg = started_msg(&listener)?; diff --git a/src/clock.rs b/src/clock.rs new file mode 100644 index 0000000..e53d825 --- /dev/null +++ b/src/clock.rs @@ -0,0 +1,51 @@ +use axum::{ + extract::{Extension, FromRequestParts, Request}, + http::{request::Parts, StatusCode}, + middleware::Next, + response::Response, +}; +use chrono::{DateTime, Utc}; + +/// Extractor that provides the "current time" for a request. This time is calculated +/// once per request, even if the extractor is used in multiple places. This requires +/// the [middleware] function to be installed with [axum::middleware::from_fn] around +/// the current route. +#[derive(Clone)] +pub struct RequestedAt(pub DateTime<Utc>); + +impl RequestedAt { + fn now() -> Self { + Self(Utc::now()) + } + + pub fn timestamp(&self) -> DateTime<Utc> { + self.0 + } +} + +#[async_trait::async_trait] +impl<S> FromRequestParts<S> for RequestedAt +where + S: Send + Sync, +{ + type Rejection = <Extension<RequestedAt> as FromRequestParts<S>>::Rejection; + + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> { + // This is purely for ergonomics: it allows `RequestedAt` to be extracted + // without having to wrap it in `Extension<>`. Callers _can_ still do that, + // but they aren't forced to. + let Extension(requested_at) = + Extension::<RequestedAt>::from_request_parts(parts, state).await?; + + Ok(requested_at) + } +} + +/// Computes a canonical "requested at" time for each request it wraps. This +/// time can be recovered using the [RequestedAt] extractor. +pub async fn middleware(mut req: Request, next: Next) -> Result<Response, StatusCode> { + let now = RequestedAt::now(); + req.extensions_mut().insert(now); + + Ok(next.run(req).await) +} @@ -1,4 +1,5 @@ pub mod cli; +mod clock; mod error; mod id; mod index; diff --git a/src/login/extract/login.rs b/src/login/extract/login.rs index f49933a..ce820f1 100644 --- a/src/login/extract/login.rs +++ b/src/login/extract/login.rs @@ -6,6 +6,7 @@ use axum::{ use sqlx::sqlite::SqlitePool; use crate::{ + clock::RequestedAt, error::InternalError, login::{ extract::IdentityToken, @@ -22,11 +23,13 @@ impl FromRequestParts<SqlitePool> for Login { state: &SqlitePool, ) -> Result<Self, Self::Rejection> { let identity_token = IdentityToken::from_request_parts(parts, state).await?; + let requested_at = RequestedAt::from_request_parts(parts, state).await?; let token = identity_token.token().ok_or(LoginError::Forbidden)?; let db = State::<SqlitePool>::from_request_parts(parts, state).await?; let mut tx = db.begin().await?; + tx.tokens().expire(requested_at.timestamp()).await?; let login = tx.tokens().validate(token).await?; tx.commit().await?; diff --git a/src/login/repo/tokens.rs b/src/login/repo/tokens.rs index 584f6dc..3ec3d63 100644 --- a/src/login/repo/tokens.rs +++ b/src/login/repo/tokens.rs @@ -1,3 +1,4 @@ +use chrono::TimeDelta; use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction}; use uuid::Uuid; @@ -62,6 +63,23 @@ impl<'c> Tokens<'c> { Ok(()) } + pub async fn expire(&mut self, expire_at: DateTime) -> Result<(), BoxedError> { + // Somewhat arbitrarily, expire after 90 days. + let expired_issue_at = expire_at - TimeDelta::days(90); + sqlx::query!( + r#" + delete + from token + where issued_at < $1 + "#, + expired_issue_at, + ) + .execute(&mut *self.0) + .await?; + + Ok(()) + } + /// Validate a token by its secret, retrieving the associated Login record. /// Will return [None] if the token is not valid. pub async fn validate(&mut self, secret: &str) -> Result<Option<Login>, BoxedError> { diff --git a/src/login/routes.rs b/src/login/routes.rs index a00982d..2269ea6 100644 --- a/src/login/routes.rs +++ b/src/login/routes.rs @@ -5,10 +5,9 @@ use axum::{ routing::post, Router, }; -use chrono::Utc; use sqlx::sqlite::SqlitePool; -use crate::error::InternalError; +use crate::{clock::RequestedAt, error::InternalError}; use super::{ extract::IdentityToken, @@ -29,11 +28,10 @@ struct Login { async fn on_login( State(db): State<SqlitePool>, + RequestedAt(now): RequestedAt, identity: IdentityToken, Form(form): Form<Login>, ) -> Result<impl IntoResponse, InternalError> { - let now = Utc::now(); - if identity.token().is_some() { return Ok((StatusCode::BAD_REQUEST, identity, "already logged in")); } |
