totp auth work
This commit is contained in:
parent
a1ae83fa5f
commit
19332e519b
|
@ -32,7 +32,7 @@ from_name = "Trifid"
|
||||||
# (Required) The address to send the email from
|
# (Required) The address to send the email from
|
||||||
from_email = "core@e3t.cc"
|
from_email = "core@e3t.cc"
|
||||||
# (Required) The email template to use. %TOKEN% will be replaced with the magic link token.
|
# (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?
|
# (Required) Should STARTTLS be used?
|
||||||
starttls = false
|
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"
|
# (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.
|
# before they must re-identify via magic link.
|
||||||
session_token_expiry_seconds = 31536000 # ~1 year
|
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
|
|
@ -0,0 +1 @@
|
||||||
|
DROP TABLE auth_tokens;
|
|
@ -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
|
||||||
|
);
|
|
@ -42,4 +42,5 @@ pub struct ConfigEmail {
|
||||||
pub struct ConfigTokens {
|
pub struct ConfigTokens {
|
||||||
pub magic_link_expiry_seconds: u64,
|
pub magic_link_expiry_seconds: u64,
|
||||||
pub session_token_expiry_seconds: u64,
|
pub session_token_expiry_seconds: u64,
|
||||||
|
pub auth_token_expiry_seconds: u64
|
||||||
}
|
}
|
||||||
|
|
|
@ -134,6 +134,8 @@ async fn main() {
|
||||||
.service(routes::v1::auth::verify_magic_link::verify_link_req)
|
.service(routes::v1::auth::verify_magic_link::verify_link_req)
|
||||||
.service(routes::v1::auth::magic_link::login_req)
|
.service(routes::v1::auth::magic_link::login_req)
|
||||||
.service(routes::v1::totp_authenticators::create_totp_auth_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(Logger::default())
|
||||||
.wrap(actix_cors::Cors::permissive())
|
.wrap(actix_cors::Cors::permissive())
|
||||||
.app_data(app_state.clone())
|
.app_data(app_state.clone())
|
||||||
|
|
|
@ -45,3 +45,15 @@ pub struct TotpAuthenticator {
|
||||||
pub secret: String,
|
pub secret: String,
|
||||||
pub verified: bool,
|
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,
|
||||||
|
}
|
|
@ -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 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 struct TotpAuthReq {
|
||||||
pub code: String
|
pub code: String
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize, Debug)]
|
||||||
pub struct TotpAuthRespMeta {}
|
pub struct TotpAuthRespMeta {}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize, Debug)]
|
||||||
pub struct TotpAuthRespData {
|
pub struct TotpAuthRespData {
|
||||||
#[serde(rename = "authToken")]
|
#[serde(rename = "authToken")]
|
||||||
pub auth_token: String,
|
pub auth_token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize, Debug)]
|
||||||
pub struct TotpAuthResp {
|
pub struct TotpAuthResp {
|
||||||
pub data: TotpAuthRespData,
|
pub data: TotpAuthRespData,
|
||||||
pub metadata: TotpAuthRespMeta,
|
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 {}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
|
@ -5,7 +5,7 @@ use crate::{randid, AppState};
|
||||||
use actix_web::http::StatusCode;
|
use actix_web::http::StatusCode;
|
||||||
use actix_web::post;
|
use actix_web::post;
|
||||||
use actix_web::web::{Data, Json};
|
use actix_web::web::{Data, Json};
|
||||||
use diesel::{ExpressionMethods, QueryDsl, SelectableHelper};
|
use diesel::QueryDsl;
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use diesel::result::OptionalExtension;
|
use diesel::result::OptionalExtension;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod signup;
|
pub mod signup;
|
||||||
pub mod totp_authenticators;
|
pub mod totp_authenticators;
|
||||||
|
pub mod verify_totp_authenticator;
|
||||||
|
|
|
@ -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 {}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,5 +1,13 @@
|
||||||
// @generated automatically by Diesel CLI.
|
// @generated automatically by Diesel CLI.
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
auth_tokens (id) {
|
||||||
|
id -> Varchar,
|
||||||
|
user_id -> Varchar,
|
||||||
|
expires -> Timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
magic_links (id) {
|
magic_links (id) {
|
||||||
id -> Varchar,
|
id -> Varchar,
|
||||||
|
@ -32,11 +40,13 @@ diesel::table! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
diesel::joinable!(auth_tokens -> users (user_id));
|
||||||
diesel::joinable!(magic_links -> users (user_id));
|
diesel::joinable!(magic_links -> users (user_id));
|
||||||
diesel::joinable!(session_tokens -> users (user_id));
|
diesel::joinable!(session_tokens -> users (user_id));
|
||||||
diesel::joinable!(totp_authenticators -> users (user_id));
|
diesel::joinable!(totp_authenticators -> users (user_id));
|
||||||
|
|
||||||
diesel::allow_tables_to_appear_in_same_query!(
|
diesel::allow_tables_to_appear_in_same_query!(
|
||||||
|
auth_tokens,
|
||||||
magic_links,
|
magic_links,
|
||||||
session_tokens,
|
session_tokens,
|
||||||
totp_authenticators,
|
totp_authenticators,
|
||||||
|
|
Loading…
Reference in New Issue