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
|
||||
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
|
|
@ -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 magic_link_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::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())
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -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 {}
|
||||
}
|
||||
)
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
pub mod auth;
|
||||
pub mod signup;
|
||||
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.
|
||||
|
||||
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,
|
||||
|
|
Loading…
Reference in New Issue