totp auth work
/ build (push) Successful in 47s Details
/ build_x64 (push) Successful in 2m6s Details
/ build_arm64 (push) Successful in 2m32s Details
/ build_win64 (push) Successful in 2m38s Details

This commit is contained in:
core 2023-11-22 10:59:21 -05:00
parent a1ae83fa5f
commit 19332e519b
Signed by: core
GPG Key ID: FDBF740DADDCEECF
11 changed files with 207 additions and 7 deletions

View File

@ -32,7 +32,7 @@ from_name = "Trifid"
# (Required) The address to send the email from
from_email = "core@e3t.cc"
# (Required) The email template to use. %TOKEN% will be replaced with the magic link token.
template = "Click this link to sign in! http://localhost:5173/magic-link?magicLinkToken=%TOKEN%"
template = "Click this link to sign in! http://localhost:5173/magic-link?magicLinkToken=%TOKEN%\nAlternatively, use `%TOKEN%` with tfcli to log in."
# (Required) Should STARTTLS be used?
starttls = false
@ -43,3 +43,6 @@ magic_link_expiry_seconds = 3600 # 1 hour
# (Required) How long should session tokens be valid for, in seconds? This controls how long users can remain "identified"
# before they must re-identify via magic link.
session_token_expiry_seconds = 31536000 # ~1 year
# (Required) How long should auth tokens be valid for, in seconds? This controls how long users can remain logged in
# before they must re-authenticate via 2FA.
auth_token_expiry_seconds = 86400 # 24 hours

View File

@ -0,0 +1 @@
DROP TABLE auth_tokens;

View File

@ -0,0 +1,6 @@
CREATE TABLE auth_tokens
(
id VARCHAR NOT NULL PRIMARY KEY,
user_id VARCHAR NOT NULL REFERENCES users (id) ON DELETE CASCADE,
expires TIMESTAMP NOT NULL
);

View File

@ -42,4 +42,5 @@ pub struct ConfigEmail {
pub struct ConfigTokens {
pub magic_link_expiry_seconds: u64,
pub session_token_expiry_seconds: u64,
pub auth_token_expiry_seconds: u64
}

View File

@ -134,6 +134,8 @@ async fn main() {
.service(routes::v1::auth::verify_magic_link::verify_link_req)
.service(routes::v1::auth::magic_link::login_req)
.service(routes::v1::totp_authenticators::create_totp_auth_req)
.service(routes::v1::verify_totp_authenticator::verify_totp_req)
.service(routes::v1::auth::totp::totp_req)
.wrap(Logger::default())
.wrap(actix_cors::Cors::permissive())
.app_data(app_state.clone())

View File

@ -45,3 +45,15 @@ pub struct TotpAuthenticator {
pub secret: String,
pub verified: bool,
}
#[derive(
Queryable, Selectable, Insertable, Identifiable, Associations, Debug, PartialEq, Clone,
)]
#[diesel(belongs_to(User))]
#[diesel(table_name = crate::schema::auth_tokens)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct AuthToken {
pub id: String,
pub user_id: String,
pub expires: SystemTime,
}

View File

@ -1,21 +1,82 @@
use std::time::{Duration, SystemTime};
use actix_web::http::StatusCode;
use actix_web::{HttpRequest, post};
use actix_web::web::{Data, Json};
use serde::{Deserialize, Serialize};
use crate::{AppState, auth, enforce, randid};
use crate::response::JsonAPIResponse;
use diesel::{QueryDsl, ExpressionMethods, SelectableHelper, BelongingToDsl};
use diesel_async::RunQueryDsl;
use totp_rs::{Algorithm, Secret, TOTP};
use crate::schema::{auth_tokens, users};
use crate::models::{AuthToken, TotpAuthenticator, User};
#[derive(Deserialize)]
#[derive(Deserialize, Debug)]
pub struct TotpAuthReq {
pub code: String
}
#[derive(Serialize)]
#[derive(Serialize, Debug)]
pub struct TotpAuthRespMeta {}
#[derive(Serialize)]
#[derive(Serialize, Debug)]
pub struct TotpAuthRespData {
#[serde(rename = "authToken")]
pub auth_token: String,
}
#[derive(Serialize)]
#[derive(Serialize, Debug)]
pub struct TotpAuthResp {
pub data: TotpAuthRespData,
pub metadata: TotpAuthRespMeta,
}
#[post("/v1/auth/totp")]
pub async fn totp_req(req: Json<TotpAuthReq>, state: Data<AppState>, req_info: HttpRequest) -> JsonAPIResponse<TotpAuthResp> {
let mut conn = handle_error!(state.pool.get().await);
let auth_info = auth!(req_info, conn);
let session_token = enforce!(sess auth_info);
let user = handle_error!(users::table.find(&session_token.user_id).first::<User>(&mut conn).await);
let authenticators: Vec<TotpAuthenticator> = handle_error!(TotpAuthenticator::belonging_to(&user).load::<TotpAuthenticator>(&mut conn).await);
let mut found_valid_code = false;
for totp_auther in authenticators {
if totp_auther.verified {
let secret = Secret::Encoded(totp_auther.secret);
let totp_machine = handle_error!(TOTP::new(Algorithm::SHA1, 6, 1, 30, handle_error!(secret.to_bytes()), Some("Trifid".to_string()), user.email.clone()));
let is_valid = handle_error!(totp_machine.check_current(&req.code));
if is_valid { found_valid_code = true; break; }
}
}
if !found_valid_code {
err!(StatusCode::UNAUTHORIZED, make_err!("ERR_UNAUTHORIZED", "unauthorized"));
}
// issue auth token
let new_token = AuthToken {
id: randid!(token "auth"),
user_id: user.id.clone(),
expires: SystemTime::now()
+ Duration::from_secs(state.config.tokens.auth_token_expiry_seconds),
};
handle_error!(
diesel::insert_into(auth_tokens::table)
.values(&new_token)
.execute(&mut conn)
.await
);
ok!(
TotpAuthResp {
data: TotpAuthRespData {
auth_token: new_token.id.clone()
},
metadata: TotpAuthRespMeta {}
}
)
}

View File

@ -5,7 +5,7 @@ use crate::{randid, AppState};
use actix_web::http::StatusCode;
use actix_web::post;
use actix_web::web::{Data, Json};
use diesel::{ExpressionMethods, QueryDsl, SelectableHelper};
use diesel::QueryDsl;
use diesel_async::RunQueryDsl;
use serde::{Deserialize, Serialize};
use diesel::result::OptionalExtension;

View File

@ -1,3 +1,4 @@
pub mod auth;
pub mod signup;
pub mod totp_authenticators;
pub mod verify_totp_authenticator;

View File

@ -0,0 +1,103 @@
use std::time::{Duration, SystemTime};
use actix_web::http::StatusCode;
use actix_web::{HttpRequest, post};
use actix_web::web::{Data, Json};
use serde::{Deserialize, Serialize};
use crate::{AppState, auth, enforce, randid};
use crate::response::JsonAPIResponse;
use diesel::{QueryDsl, ExpressionMethods, SelectableHelper, OptionalExtension};
use diesel_async::RunQueryDsl;
use totp_rs::{Algorithm, Secret, TOTP};
use crate::schema::{auth_tokens, totp_authenticators, users};
use crate::models::{AuthToken, TotpAuthenticator, User};
#[derive(Deserialize, Debug)]
pub struct VerifyTotpAuthReq {
#[serde(rename = "totpToken")]
pub totp_token: String,
pub code: String
}
#[derive(Serialize, Debug)]
pub struct TotpAuthRespMeta {}
#[derive(Serialize, Debug)]
pub struct TotpAuthRespData {
#[serde(rename = "authToken")]
pub auth_token: String,
}
#[derive(Serialize, Debug)]
pub struct TotpAuthResp {
pub data: TotpAuthRespData,
pub metadata: TotpAuthRespMeta,
}
#[post("/v1/verify-totp-authenticator")]
pub async fn verify_totp_req(req: Json<VerifyTotpAuthReq>, state: Data<AppState>, req_info: HttpRequest) -> JsonAPIResponse<TotpAuthResp> {
let mut conn = handle_error!(state.pool.get().await);
let auth_info = auth!(req_info, conn);
let session_token = enforce!(sess auth_info);
let user = handle_error!(users::table.find(&session_token.user_id).first::<User>(&mut conn).await);
let authenticator = match handle_error!(totp_authenticators::table.find(&req.totp_token).first::<TotpAuthenticator>(&mut conn).await.optional()) {
Some(t) => t,
None => {
err!(
StatusCode::BAD_REQUEST,
make_err!(
"ERR_INVALID_TOTP_TOKEN",
"TOTP token does not exist (maybe it expired?)",
"totpToken"
)
)
}
};
if authenticator.verified {
err!(
StatusCode::BAD_REQUEST,
make_err!(
"ERR_INVALID_TOTP_TOKEN",
"TOTP token already verified",
"totpToken"
)
);
}
let secret = Secret::Encoded(authenticator.secret.clone());
let totp_machine = handle_error!(TOTP::new(Algorithm::SHA1, 6, 1, 30, handle_error!(secret.to_bytes()), Some("Trifid".to_string()), user.email.clone()));
let is_valid = handle_error!(totp_machine.check_current(&req.code));
if !is_valid {
err!(StatusCode::UNAUTHORIZED, make_err!("ERR_UNAUTHORIZED", "unauthorized"));
}
handle_error!(diesel::update(&authenticator).set(totp_authenticators::dsl::verified.eq(true)).execute(&mut conn).await);
// issue auth token
let new_token = AuthToken {
id: randid!(token "auth"),
user_id: user.id.clone(),
expires: SystemTime::now()
+ Duration::from_secs(state.config.tokens.auth_token_expiry_seconds),
};
handle_error!(
diesel::insert_into(auth_tokens::table)
.values(&new_token)
.execute(&mut conn)
.await
);
ok!(
TotpAuthResp {
data: TotpAuthRespData {
auth_token: new_token.id.clone()
},
metadata: TotpAuthRespMeta {}
}
)
}

View File

@ -1,5 +1,13 @@
// @generated automatically by Diesel CLI.
diesel::table! {
auth_tokens (id) {
id -> Varchar,
user_id -> Varchar,
expires -> Timestamp,
}
}
diesel::table! {
magic_links (id) {
id -> Varchar,
@ -32,11 +40,13 @@ diesel::table! {
}
}
diesel::joinable!(auth_tokens -> users (user_id));
diesel::joinable!(magic_links -> users (user_id));
diesel::joinable!(session_tokens -> users (user_id));
diesel::joinable!(totp_authenticators -> users (user_id));
diesel::allow_tables_to_appear_in_same_query!(
auth_tokens,
magic_links,
session_tokens,
totp_authenticators,