add auth middleware and identity endpoint

This commit is contained in:
anna 2022-12-19 21:43:12 +01:00
parent d6fc5d373a
commit 271a912879
Signed by: fef
GPG key ID: EC22E476DC2D3D84
8 changed files with 204 additions and 13 deletions

49
Cargo.lock generated
View file

@ -643,6 +643,21 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "futures"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.25"
@ -659,6 +674,17 @@ version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac"
[[package]]
name = "futures-executor"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-intrusive"
version = "0.4.2"
@ -670,6 +696,23 @@ dependencies = [
"parking_lot 0.11.2",
]
[[package]]
name = "futures-io"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb"
[[package]]
name = "futures-macro"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "futures-sink"
version = "0.3.25"
@ -688,11 +731,16 @@ version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
@ -1080,6 +1128,7 @@ dependencies = [
"async-trait",
"chrono",
"dotenvy",
"futures",
"jsonwebtoken",
"log",
"pretty_env_logger",

View file

@ -10,6 +10,7 @@ argon2 = "0.4"
async-trait = "0.1.59"
chrono = { version = "0.4", features = [ "alloc", "clock", "serde" ] }
dotenvy = "0.15.6"
futures = "0.3"
jsonwebtoken = { version = "8", default-features = false }
log = "0.4"
pretty_env_logger = "0.4"

View file

@ -1,8 +1,9 @@
use actix_web::body::BoxBody;
use actix_web::http::StatusCode;
use actix_web::http::{header, StatusCode};
use actix_web::{HttpResponse, ResponseError};
use chrono::prelude::*;
use serde::{Serialize, Serializer};
use std::time::{SystemTime, UNIX_EPOCH};
use std::{fmt, io};
use crate::util::validate;
@ -29,6 +30,15 @@ pub fn utc_now() -> NaiveDateTime {
Utc::now().naive_utc()
}
pub fn unix_now() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect(
"You've either broken spacetime, or your system clock is a bit off.",
)
.as_secs() as i64
}
impl ResponseError for Error {
fn status_code(&self) -> StatusCode {
match self {

View file

@ -18,6 +18,9 @@ mod data;
/// Asynchronous background workers.
mod job;
/// Middleware for request handlers.
mod middle;
/// Database models and validation.
mod model;
@ -65,6 +68,7 @@ async fn main() -> std::io::Result<()> {
HttpServer::new(move || {
App::new()
.wrap(actix_web::middleware::DefaultHeaders::new().add(("Server", "nyano")))
.wrap(middle::auth::Auth::new(state.clone()))
.app_data(state.clone())
.configure(route::configure)
})

113
src/middle/auth.rs Normal file
View file

@ -0,0 +1,113 @@
use actix_web::dev::{Payload, Service, ServiceRequest, ServiceResponse, Transform};
use actix_web::{FromRequest, HttpMessage, HttpRequest};
use futures::future::LocalBoxFuture;
use futures::FutureExt;
use std::future::{ready, Ready};
use std::rc::Rc;
use std::task::{Context, Poll};
use crate::core::*;
use crate::model::Account;
use crate::state::AppState;
use crate::util::token;
/// Factory for [`AuthMiddleware`].
pub struct Auth {
state: AppState,
}
impl Auth {
pub fn new(state: AppState) -> Auth {
Auth { state }
}
}
impl<S, B> Transform<S, ServiceRequest> for Auth
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + 'static,
{
type Response = ServiceResponse<B>;
type Error = actix_web::Error;
type Transform = AuthMiddleware<S>;
type InitError = ();
type Future = Ready<std::result::Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(AuthMiddleware {
service: Rc::new(service),
state: self.state.clone(),
}))
}
}
pub struct AuthMiddleware<S> {
service: Rc<S>,
state: AppState,
}
impl<S, B> Service<ServiceRequest> for AuthMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + 'static,
{
type Response = ServiceResponse<B>;
type Error = actix_web::Error;
type Future = LocalBoxFuture<'static, std::result::Result<Self::Response, Self::Error>>;
fn poll_ready(&self, ctx: &mut Context<'_>) -> Poll<std::result::Result<(), Self::Error>> {
self.service.poll_ready(ctx)
}
fn call(&self, req: ServiceRequest) -> Self::Future {
let service = self.service.clone();
let state = self.state.clone();
async move {
let account = if let Some(token) = req.headers().get("Authorization") {
let token = extract_token(token.to_str().unwrap())?;
debug!("token = \"{}\"", token);
let account = token::validate(&state, token).await?;
Some(account)
} else {
None
};
req.extensions_mut().insert(account);
service.call(req).await
}
.boxed_local()
}
}
pub struct AuthData(Option<Account>);
impl AuthData {
pub fn maybe(&self) -> Option<&Account> {
self.0.as_ref()
}
pub fn require(&self) -> Result<&Account> {
self.maybe().ok_or(Error::BadCredentials)
}
}
impl FromRequest for AuthData {
type Error = Error;
type Future = Ready<std::result::Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
let val: Option<Option<Account>> = req.extensions_mut().remove();
ready(match val {
Some(a) => Ok(AuthData(a)),
None => Err(Error::BadCredentials),
})
}
}
fn extract_token(header: &str) -> Result<&str> {
const PREFIX: &'static str = "Bearer ";
if header.starts_with(PREFIX) {
Ok(&header[PREFIX.len()..])
} else {
Err(Error::BadCredentials)
}
}

3
src/middle/mod.rs Normal file
View file

@ -0,0 +1,3 @@
pub mod auth;
pub use auth::AuthData;

View file

@ -2,11 +2,18 @@ use actix_web::{get, post, web, HttpResponse};
use serde::Deserialize;
use crate::core::*;
use crate::middle::AuthData;
use crate::model::{NewAccount, NewUser};
use crate::state::AppState;
use crate::util::password;
use crate::util::validate::{ResultBuilder, Validate};
#[get("/self")]
async fn get_self(account: AuthData) -> Result<HttpResponse> {
let account = account.require()?;
Ok(HttpResponse::Ok().json(account))
}
#[derive(Deserialize)]
struct SignupData {
username: String,
@ -75,5 +82,8 @@ async fn get_notes(path: web::Path<Id>, state: AppState) -> Result<HttpResponse>
}
pub fn configure(cfg: &mut web::ServiceConfig) {
cfg.service(get_by_id).service(get_notes).service(signup);
cfg.service(get_self)
.service(get_by_id)
.service(get_notes)
.service(signup);
}

View file

@ -1,4 +1,3 @@
use chrono::{DateTime, Duration, Utc};
use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
@ -6,18 +5,20 @@ use crate::core::*;
use crate::model::Account;
use crate::state::AppState;
const VALID_DAYS: i64 = 30;
const VALID_SECS: i64 = 30 * 86400; // 30 days
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
acct: String, // store as a string because JSON can't be trusted
exp: DateTime<Utc>,
/// Account ID, stored as a string because JSON can't be trusted
sub: String,
/// Expiry date in UNIX time (seconds)
exp: i64,
}
pub fn issue(state: &AppState, account: &Account) -> Result<String> {
let claims = Claims {
acct: format!("{}", account.id),
exp: Utc::now() + Duration::days(VALID_DAYS),
sub: format!("{}", account.id),
exp: unix_now() + 30 * 86400,
};
let encoding_key = EncodingKey::from_secret(state.config.jwt_secret.as_slice());
let header = Header {
@ -29,14 +30,14 @@ pub fn issue(state: &AppState, account: &Account) -> Result<String> {
Ok(encode(&header, &claims, &encoding_key)?)
}
async fn validate(state: &AppState, token: &str) -> Result<Account> {
pub async fn validate(state: &AppState, token: &str) -> Result<Account> {
let decoding_key = DecodingKey::from_secret(state.config.jwt_secret.as_slice());
let validation = Validation::new(Algorithm::HS256);
let claims: Claims = decode(token, &decoding_key, &validation)?.claims;
if claims.exp - Utc::now() > Duration::days(VALID_DAYS) {
// tokens that are valid for longer than VALID_DAYS are sus
todo!()
if unix_now() - claims.exp > VALID_SECS {
// tokens that are valid for longer than VALID_SECS are sus
return Err(Error::BadCredentials);
}
let account_id: Id = claims.acct.parse().expect("We issued an invalid token??");
let account_id: Id = claims.sub.parse().expect("We issued an invalid token??");
state.repo.accounts.by_id(account_id).await
}