authentication

This commit is contained in:
Aiden McClelland
2021-07-29 12:29:01 -06:00
parent 0b579d9813
commit ce9495c6cc
7 changed files with 277 additions and 81 deletions

View File

@@ -9,7 +9,13 @@ CREATE TABLE IF NOT EXISTS tor
CREATE TABLE IF NOT EXISTS session CREATE TABLE IF NOT EXISTS session
( (
id TEXT NOT NULL PRIMARY KEY, id TEXT NOT NULL PRIMARY KEY,
created_at TIMESTAMP NOT NULL, logged_in TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NOT NULL, logged_out TIMESTAMP,
metadata JSON last_active TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
user_agent TEXT,
metadata TEXT NOT NULL DEFAULT 'null'
);
CREATE TABLE IF NOT EXISTS password
(
hash TEXT NOT NULL PRIMARY KEY
); );

196
appmgr/src/auth.rs Normal file
View File

@@ -0,0 +1,196 @@
use anyhow::anyhow;
use basic_cookies::Cookie;
use chrono::NaiveDateTime;
use clap::ArgMatches;
use http::header::COOKIE;
use http::HeaderValue;
use indexmap::IndexMap;
use rpc_toolkit::command;
use rpc_toolkit::command_helpers::prelude::{RequestParts, ResponseParts};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::context::EitherContext;
use crate::middleware::auth::{get_id, hash_token};
use crate::util::{display_none, display_serializable, IoFormat};
use crate::{Error, ResultExt};
#[command(subcommands(login, logout))]
pub fn auth(#[context] ctx: EitherContext) -> Result<EitherContext, Error> {
Ok(ctx)
}
pub fn parse_metadata(_: &str, _: &ArgMatches<'_>) -> Result<Value, Error> {
Ok(serde_json::json!({
"platforms": ["cli"],
}))
}
#[command(display(display_none), metadata(authenticated = false))]
pub async fn login(
#[context] ctx: EitherContext,
#[request] req: &RequestParts,
#[response] res: &mut ResponseParts,
#[arg] password: String,
#[arg(
parse(parse_metadata),
default = "",
help = "RPC Only: This value cannot be overidden from the cli"
)]
metadata: Value,
) -> Result<(), Error> {
let rpc_ctx = ctx.as_rpc().unwrap();
let mut handle = rpc_ctx.secret_store.acquire().await?;
let pw_hash = sqlx::query!("SELECT hash FROM password")
.fetch_one(&mut handle)
.await?
.hash;
argon2::verify_encoded(&pw_hash, password.as_bytes()).map_err(|_| {
Error::new(
anyhow!("Password Incorrect"),
crate::ErrorKind::Authorization,
)
})?;
let token = base32::encode(
base32::Alphabet::RFC4648 { padding: false },
&rand::random::<[u8; 16]>(),
)
.to_lowercase();
let id = hash_token(&token);
let user_agent = req.headers.get("user-agent").and_then(|h| h.to_str().ok());
let metadata = serde_json::to_string(&metadata).with_kind(crate::ErrorKind::Database)?;
sqlx::query!(
"INSERT INTO session (id, user_agent, metadata) VALUES (?, ?, ?)",
id,
user_agent,
metadata,
)
.execute(&mut handle)
.await?;
res.headers.insert(
"set-cookie",
HeaderValue::from_str(&format!("session={}; HttpOnly; SameSite=Strict", token))
.with_kind(crate::ErrorKind::Unknown)?, // Should be impossible, but don't want to panic
);
Ok(())
}
#[command(display(display_none), metadata(authenticated = false))]
pub async fn logout(
#[context] ctx: EitherContext,
#[request] req: &RequestParts,
) -> Result<(), Error> {
if let Some(cookie_header) = req.headers.get(COOKIE) {
let cookies = Cookie::parse(
cookie_header
.to_str()
.with_kind(crate::ErrorKind::Authorization)?,
)
.with_kind(crate::ErrorKind::Authorization)?;
if let Some(session) = cookies.iter().find(|c| c.get_name() == "session") {
let token = session.get_value();
let id = hash_token(token);
kill(ctx, id).await?;
}
}
Ok(())
}
#[derive(Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct Session {
logged_in: NaiveDateTime,
last_active: NaiveDateTime,
user_agent: Option<String>,
metadata: Value,
}
#[derive(Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct SessionList {
current: String,
sessions: IndexMap<String, Session>,
}
#[command(subcommands(list, kill))]
pub async fn session(#[context] ctx: EitherContext) -> Result<EitherContext, Error> {
Ok(ctx)
}
fn display_sessions(arg: SessionList, matches: &ArgMatches<'_>) {
use prettytable::*;
if matches.is_present("format") {
return display_serializable(arg, matches);
}
let mut table = Table::new();
table.add_row(row![bc =>
"ID",
"LOGGED IN",
"LAST ACTIVE",
"USER AGENT",
"METADATA",
]);
for (id, session) in arg.sessions {
let mut row = row![
&id,
&format!("{}", session.logged_in),
&format!("{}", session.last_active),
session.user_agent.as_deref().unwrap_or("N/A"),
&format!("{}", session.metadata),
];
if id == arg.current {
row.iter_mut()
.map(|c| c.style(Attr::ForegroundColor(color::GREEN)))
.collect::<()>()
}
table.add_row(row);
}
table.print_tty(false);
}
#[command(display(display_sessions))]
pub async fn list(
#[context] ctx: EitherContext,
#[request] req: &RequestParts,
#[allow(unused_variables)]
#[arg(long = "format")]
format: Option<IoFormat>,
) -> Result<SessionList, Error> {
Ok(SessionList {
current: get_id(req)?,
sessions: sqlx::query!(
"SELECT * FROM session WHERE logged_out IS NULL OR logged_out > CURRENT_TIMESTAMP"
)
.fetch_all(&mut ctx.as_rpc().unwrap().secret_store.acquire().await?)
.await?
.into_iter()
.map(|row| {
Ok((
row.id,
Session {
logged_in: row.logged_in,
last_active: row.last_active,
user_agent: row.user_agent,
metadata: serde_json::from_str(&row.metadata)
.with_kind(crate::ErrorKind::Database)?,
},
))
})
.collect::<Result<_, Error>>()?,
})
}
#[command(display(display_none))]
pub async fn kill(#[context] ctx: EitherContext, #[arg] id: String) -> Result<(), Error> {
let rpc_ctx = ctx.as_rpc().unwrap();
sqlx::query!(
"UPDATE session SET logged_out = CURRENT_TIMESTAMP WHERE id = ?",
id
)
.execute(&mut rpc_ctx.secret_store.acquire().await?)
.await?;
Ok(())
}

View File

@@ -9,7 +9,7 @@ use crate::util::{display_none, display_serializable, IoFormat};
use crate::Error; use crate::Error;
#[command(subcommands(hash, manifest, license, icon, instructions, docker_images))] #[command(subcommands(hash, manifest, license, icon, instructions, docker_images))]
pub fn inspect(#[context] ctx: EitherContext) -> Result<(), Error> { pub fn inspect(#[context] _ctx: EitherContext) -> Result<(), Error> {
Ok(()) Ok(())
} }

View File

@@ -17,6 +17,7 @@ lazy_static::lazy_static! {
} }
pub mod action; pub mod action;
pub mod auth;
pub mod backup; pub mod backup;
pub mod config; pub mod config;
pub mod context; pub mod context;

View File

@@ -1,22 +1,25 @@
use anyhow::anyhow; use anyhow::anyhow;
use basic_cookies::Cookie; use basic_cookies::Cookie;
use chrono::Utc; use chrono::Utc;
use digest::Digest;
use futures::future::BoxFuture; use futures::future::BoxFuture;
use futures::FutureExt; use futures::FutureExt;
use rpc_toolkit::command_helpers::prelude::RequestParts;
use rpc_toolkit::hyper::header::COOKIE; use rpc_toolkit::hyper::header::COOKIE;
use rpc_toolkit::hyper::http::Error as HttpError; use rpc_toolkit::hyper::http::Error as HttpError;
use rpc_toolkit::hyper::{Body, Request, Response, StatusCode}; use rpc_toolkit::hyper::{Body, Request, Response};
use rpc_toolkit::rpc_server_helpers::{ use rpc_toolkit::rpc_server_helpers::{
noop2, noop3, DynMiddleware, DynMiddlewareStage2, DynMiddlewareStage3, noop3, noop4, DynMiddleware, DynMiddlewareStage2, DynMiddlewareStage3,
}; };
use rpc_toolkit::yajrc::RpcMethod; use rpc_toolkit::yajrc::RpcMethod;
use rpc_toolkit::Metadata; use rpc_toolkit::Metadata;
use serde::Deserialize; use sha2::Sha256;
use crate::context::RpcContext; use crate::context::RpcContext;
use crate::{Error, ResultExt}; use crate::{Error, ResultExt};
async fn is_authed(ctx: &RpcContext, req: &Request<Body>) -> Result<bool, Error> {
if let Some(cookie_header) = req.headers().get(COOKIE) { pub fn get_id(req: &RequestParts) -> Result<String, Error> {
if let Some(cookie_header) = req.headers.get(COOKIE) {
let cookies = Cookie::parse( let cookies = Cookie::parse(
cookie_header cookie_header
.to_str() .to_str()
@@ -24,80 +27,69 @@ async fn is_authed(ctx: &RpcContext, req: &Request<Body>) -> Result<bool, Error>
) )
.with_kind(crate::ErrorKind::Authorization)?; .with_kind(crate::ErrorKind::Authorization)?;
if let Some(session) = cookies.iter().find(|c| c.get_name() == "session") { if let Some(session) = cookies.iter().find(|c| c.get_name() == "session") {
let id = session.get_value(); return Ok(hash_token(session.get_value()));
let exp = sqlx::query!("SELECT expires_at FROM session WHERE id = ?", id)
.fetch_one(&mut ctx.secret_store.acquire().await?)
.await?;
if exp.expires_at < Utc::now().naive_utc() {
return Ok(true);
}
} }
} }
Ok(false) Err(Error::new(
anyhow!("UNAUTHORIZED"),
crate::ErrorKind::Authorization,
))
} }
pub async fn auth<Params: for<'de> Deserialize<'de> + 'static, M: Metadata>( pub fn hash_token(token: &str) -> String {
ctx: RpcContext, let mut hasher = Sha256::new();
) -> DynMiddleware<Params, M> { hasher.update(token.as_bytes());
base32::encode(
base32::Alphabet::RFC4648 { padding: false },
hasher.finalize().as_slice(),
)
.to_lowercase()
}
async fn is_authed(ctx: &RpcContext, req: &RequestParts) -> Result<(), Error> {
let id = get_id(req)?;
let exp = sqlx::query!("SELECT logged_out FROM session WHERE id = ?", id)
.fetch_one(&mut ctx.secret_store.acquire().await?)
.await?;
match exp.logged_out {
Some(exp) if exp >= Utc::now().naive_utc() => Err(Error::new(
anyhow!("UNAUTHORIZED"),
crate::ErrorKind::Authorization,
)),
_ => Ok(()),
}
}
pub async fn auth<M: Metadata>(ctx: RpcContext) -> DynMiddleware<M> {
Box::new( Box::new(
|req: &mut Request<Body>, |req: &mut Request<Body>,
metadata: M| metadata: M|
-> BoxFuture< -> BoxFuture<Result<Result<DynMiddlewareStage2, Response<Body>>, HttpError>> {
Result<Result<DynMiddlewareStage2<Params>, Response<Body>>, HttpError>,
> {
async move { async move {
match is_authed(&ctx, req).await { let mut header_stub = Request::new(Body::empty());
Ok(true) => Ok(Ok(noop2())), *header_stub.headers_mut() = req.headers().clone();
Ok(false) => Ok(Ok({ let m2: DynMiddlewareStage2 = Box::new(move |req, rpc_req| {
let mut fake_req = Request::new(Body::empty()); async move {
*fake_req.headers_mut() = req.headers().clone(); if metadata
let m2: DynMiddlewareStage2<Params> = .get(rpc_req.method.as_str(), "authenticated")
Box::new(move |rpc_req| { .unwrap_or(true)
let method = rpc_req.method.as_str(); {
let res: Result< if let Err(e) = is_authed(&ctx, req).await {
Result<DynMiddlewareStage3, Response<Body>>, let m3: DynMiddlewareStage3 = Box::new(|_, rpc_res| {
HttpError, async move {
> = if metadata.get(method, "login").unwrap_or(false) { *rpc_res = Err(e.into());
todo!("set cookie on success") Ok(Ok(noop4()))
} else if !metadata.get(method, "authenticated").unwrap_or(true) { }
Ok(Ok(noop3())) .boxed()
} else { });
rpc_toolkit::rpc_server_helpers::to_response( return Ok(Ok(m3));
&fake_req, }
Ok(( }
rpc_req.id.clone(), Ok(Ok(noop3()))
Err(Error::new( }
anyhow!("UNAUTHORIZED"), .boxed()
crate::ErrorKind::Authorization, });
) Ok(Ok(m2))
.into()),
)),
|_| StatusCode::OK,
)
.map(|a| Err(a))
};
async { res }.boxed()
});
m2
})),
Err(e) => Ok(Ok({
let mut fake_req = Request::new(Body::empty());
*fake_req.headers_mut() = req.headers().clone();
let m2: DynMiddlewareStage2<Params> = Box::new(move |rpc_req| {
let res: Result<
Result<DynMiddlewareStage3, Response<Body>>,
HttpError,
> = rpc_toolkit::rpc_server_helpers::to_response(
&fake_req,
Ok((rpc_req.id.clone(), Err(e.into()))),
|_| StatusCode::OK,
)
.map(|a| Err(a));
async { res }.boxed()
});
m2
})),
}
} }
.boxed() .boxed()
}, },

View File

@@ -4,11 +4,12 @@ use rpc_toolkit::hyper::{Body, Method, Request, Response};
use rpc_toolkit::rpc_server_helpers::{ use rpc_toolkit::rpc_server_helpers::{
DynMiddlewareStage2, DynMiddlewareStage3, DynMiddlewareStage4, DynMiddlewareStage2, DynMiddlewareStage3, DynMiddlewareStage4,
}; };
use serde::Deserialize; use rpc_toolkit::Metadata;
pub async fn cors<Params: for<'de> Deserialize<'de> + 'static, Metadata>( pub async fn cors<M: Metadata>(
req: &mut Request<Body>, req: &mut Request<Body>,
) -> Result<Result<DynMiddlewareStage2<Params>, Response<Body>>, HttpError> { _metadata: M,
) -> Result<Result<DynMiddlewareStage2, Response<Body>>, HttpError> {
if req.method() == Method::OPTIONS { if req.method() == Method::OPTIONS {
Ok(Err(Response::builder() Ok(Err(Response::builder()
.header( .header(
@@ -24,9 +25,9 @@ pub async fn cors<Params: for<'de> Deserialize<'de> + 'static, Metadata>(
.header("Access-Control-Allow-Credentials", "true") .header("Access-Control-Allow-Credentials", "true")
.body(Body::empty())?)) .body(Body::empty())?))
} else { } else {
Ok(Ok(Box::new(|_| { Ok(Ok(Box::new(|_, _| {
async move { async move {
let res: DynMiddlewareStage3 = Box::new(|_| { let res: DynMiddlewareStage3 = Box::new(|_, _| {
async move { async move {
let res: DynMiddlewareStage4 = Box::new(|res| { let res: DynMiddlewareStage4 = Box::new(|res| {
async move { async move {

View File

@@ -9,7 +9,7 @@ use std::time::Duration;
use anyhow::anyhow; use anyhow::anyhow;
use async_trait::async_trait; use async_trait::async_trait;
use clap::ArgMatches; use clap::{Arg, ArgMatches};
use digest::Digest; use digest::Digest;
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::Value; use serde_json::Value;