Files
AstralRinth/src/auth/session.rs
Geometrically 7fbb8838e7 Scoped PATs (#651)
* Scoped PATs

* fix threads issues

* fix migration
2023-07-10 16:44:40 -07:00

237 lines
6.6 KiB
Rust

use crate::auth::{get_user_from_headers, AuthenticationError};
use crate::database::models::session_item::Session as DBSession;
use crate::database::models::session_item::SessionBuilder;
use crate::database::models::UserId;
use crate::models::pats::Scopes;
use crate::models::sessions::Session;
use crate::queue::session::AuthQueue;
use crate::routes::ApiError;
use crate::util::env::parse_var;
use actix_web::http::header::AUTHORIZATION;
use actix_web::web::{scope, Data, ServiceConfig};
use actix_web::{delete, get, post, web, HttpRequest, HttpResponse};
use chrono::Utc;
use rand::distributions::Alphanumeric;
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha20Rng;
use sqlx::PgPool;
use woothee::parser::Parser;
pub fn config(cfg: &mut ServiceConfig) {
cfg.service(
scope("session")
.service(list)
.service(delete)
.service(refresh),
);
}
pub struct SessionMetadata {
pub city: Option<String>,
pub country: Option<String>,
pub ip: String,
pub os: Option<String>,
pub platform: Option<String>,
pub user_agent: String,
}
pub async fn get_session_metadata(
req: &HttpRequest,
) -> Result<SessionMetadata, AuthenticationError> {
let conn_info = req.connection_info().clone();
let ip_addr = if parse_var("CLOUDFLARE_INTEGRATION").unwrap_or(false) {
if let Some(header) = req.headers().get("CF-Connecting-IP") {
header.to_str().ok()
} else {
conn_info.peer_addr()
}
} else {
conn_info.peer_addr()
};
let country = req
.headers()
.get("cf-ipcountry")
.and_then(|x| x.to_str().ok());
let city = req.headers().get("cf-ipcity").and_then(|x| x.to_str().ok());
let user_agent = req
.headers()
.get("user-agent")
.and_then(|x| x.to_str().ok())
.ok_or_else(|| AuthenticationError::InvalidCredentials)?;
let parser = Parser::new();
let info = parser.parse(user_agent);
let os = if let Some(info) = info {
Some((info.os, info.name))
} else {
None
};
Ok(SessionMetadata {
os: os.map(|x| x.0.to_string()),
platform: os.map(|x| x.1.to_string()),
city: city.map(|x| x.to_string()),
country: country.map(|x| x.to_string()),
ip: ip_addr
.ok_or_else(|| AuthenticationError::InvalidCredentials)?
.to_string(),
user_agent: user_agent.to_string(),
})
}
pub async fn issue_session(
req: HttpRequest,
user_id: UserId,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
redis: &deadpool_redis::Pool,
) -> Result<DBSession, AuthenticationError> {
let metadata = get_session_metadata(&req).await?;
let session = ChaCha20Rng::from_entropy()
.sample_iter(&Alphanumeric)
.take(60)
.map(char::from)
.collect::<String>();
let session = format!("mra_{session}");
let id = SessionBuilder {
session,
user_id,
os: metadata.os,
platform: metadata.platform,
city: metadata.city,
country: metadata.country,
ip: metadata.ip,
user_agent: metadata.user_agent,
}
.insert(transaction)
.await?;
let session = DBSession::get_id(id, &mut *transaction, redis)
.await?
.ok_or_else(|| AuthenticationError::InvalidCredentials)?;
Ok(session)
}
#[get("list")]
pub async fn list(
req: HttpRequest,
pool: Data<PgPool>,
redis: Data<deadpool_redis::Pool>,
session_queue: Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let current_user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Some(&[Scopes::SESSION_READ]),
)
.await?
.1;
let session_ids = DBSession::get_user_sessions(current_user.id.into(), &**pool, &redis).await?;
let sessions = DBSession::get_many_ids(&session_ids, &**pool, &redis)
.await?
.into_iter()
.filter(|x| x.expires > Utc::now())
.map(|x| Session::from(x, false))
.collect::<Vec<_>>();
Ok(HttpResponse::Ok().json(sessions))
}
#[delete("{id}")]
pub async fn delete(
info: web::Path<(String,)>,
req: HttpRequest,
pool: Data<PgPool>,
redis: Data<deadpool_redis::Pool>,
session_queue: Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let current_user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Some(&[Scopes::SESSION_DELETE]),
)
.await?
.1;
let session = DBSession::get(info.into_inner().0, &**pool, &redis).await?;
if let Some(session) = session {
if session.user_id != current_user.id.into() {
let mut transaction = pool.begin().await?;
DBSession::remove(session.id, &mut transaction).await?;
DBSession::clear_cache(
vec![(
Some(session.id),
Some(session.session),
Some(session.user_id),
)],
&redis,
)
.await?;
transaction.commit().await?;
}
}
Ok(HttpResponse::NoContent().body(""))
}
#[post("refresh")]
pub async fn refresh(
req: HttpRequest,
pool: Data<PgPool>,
redis: Data<deadpool_redis::Pool>,
session_queue: Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let current_user = get_user_from_headers(&req, &**pool, &redis, &session_queue, None)
.await?
.1;
let session = req
.headers()
.get(AUTHORIZATION)
.and_then(|x| x.to_str().ok())
.ok_or_else(|| ApiError::Authentication(AuthenticationError::InvalidCredentials))?;
let session = DBSession::get(session, &**pool, &redis).await?;
if let Some(session) = session {
if current_user.id != session.user_id.into() || session.refresh_expires < Utc::now() {
return Err(ApiError::Authentication(
AuthenticationError::InvalidCredentials,
));
}
let mut transaction = pool.begin().await?;
DBSession::remove(session.id, &mut transaction).await?;
let new_session = issue_session(req, session.user_id, &mut transaction, &redis).await?;
DBSession::clear_cache(
vec![(
Some(session.id),
Some(session.session),
Some(session.user_id),
)],
&redis,
)
.await?;
transaction.commit().await?;
Ok(HttpResponse::Ok().json(Session::from(new_session, true)))
} else {
Err(ApiError::Authentication(
AuthenticationError::InvalidCredentials,
))
}
}