create final totp authentication endpoint - full authentication flow is now finished (at and st)
This commit is contained in:
parent
cf69255acd
commit
9f21980c39
|
@ -7,7 +7,7 @@ pub struct PartialUserInfo {
|
||||||
pub user_id: i32,
|
pub user_id: i32,
|
||||||
pub created_at: i64,
|
pub created_at: i64,
|
||||||
pub email: String,
|
pub email: String,
|
||||||
pub has_totp: bool,
|
pub has_totp_auth: bool,
|
||||||
|
|
||||||
pub session_id: String,
|
pub session_id: String,
|
||||||
pub auth_id: Option<String>
|
pub auth_id: Option<String>
|
||||||
|
@ -88,7 +88,7 @@ impl<'r> FromRequest<'r> for PartialUserInfo {
|
||||||
user_id: user_id as i32,
|
user_id: user_id as i32,
|
||||||
created_at: user.created_on as i64,
|
created_at: user.created_on as i64,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
has_totp: at.is_some(),
|
has_totp_auth: at.is_some(),
|
||||||
session_id: st,
|
session_id: st,
|
||||||
auth_id: at,
|
auth_id: at,
|
||||||
})
|
})
|
||||||
|
@ -113,7 +113,7 @@ impl<'r> FromRequest<'r> for TOTPAuthenticatedUserInfo {
|
||||||
Outcome::Failure(e) => Outcome::Failure(e),
|
Outcome::Failure(e) => Outcome::Failure(e),
|
||||||
Outcome::Forward(f) => Outcome::Forward(f),
|
Outcome::Forward(f) => Outcome::Forward(f),
|
||||||
Outcome::Success(s) => {
|
Outcome::Success(s) => {
|
||||||
if s.has_totp {
|
if s.has_totp_auth {
|
||||||
Outcome::Success(Self {
|
Outcome::Success(Self {
|
||||||
user_id: s.user_id,
|
user_id: s.user_id,
|
||||||
created_at: s.created_at,
|
created_at: s.created_at,
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
extern crate core;
|
||||||
|
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
@ -82,7 +84,8 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
crate::routes::v1::signup::signup_request,
|
crate::routes::v1::signup::signup_request,
|
||||||
crate::routes::v1::auth::verify_magic_link::verify_magic_link,
|
crate::routes::v1::auth::verify_magic_link::verify_magic_link,
|
||||||
crate::routes::v1::totp_authenticators::totp_authenticators_request,
|
crate::routes::v1::totp_authenticators::totp_authenticators_request,
|
||||||
crate::routes::v1::verify_totp_authenticator::verify_totp_authenticator_request
|
crate::routes::v1::verify_totp_authenticator::verify_totp_authenticator_request,
|
||||||
|
crate::routes::v1::auth::totp::totp_request
|
||||||
])
|
])
|
||||||
.register("/", catchers![
|
.register("/", catchers![
|
||||||
crate::routes::handler_400,
|
crate::routes::handler_400,
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
pub mod verify_magic_link;
|
pub mod verify_magic_link;
|
||||||
pub mod magic_link;
|
pub mod magic_link;
|
||||||
|
pub mod totp;
|
|
@ -0,0 +1,66 @@
|
||||||
|
use rocket::http::{ContentType, Status};
|
||||||
|
use rocket::serde::json::Json;
|
||||||
|
use crate::auth::PartialUserInfo;
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use rocket::{post, State};
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use crate::tokens::{generate_auth_token, get_totpmachine, user_has_totp};
|
||||||
|
|
||||||
|
pub const TOTP_GENERIC_UNAUTHORIZED_ERROR: &str = "{\"errors\":[{\"code\":\"ERR_INVALID_TOTP_CODE\",\"message\":\"invalid TOTP code (maybe it expired?)\",\"path\":\"code\"}]}";
|
||||||
|
pub const TOTP_NO_TOTP_ERROR: &str = "{\"errors\":[{\"code\":\"ERR_NO_TOTP\",\"message\":\"logged-in user does not have totp enabled\",\"path\":\"code\"}]}";
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(crate = "rocket::serde")]
|
||||||
|
pub struct TotpRequest {
|
||||||
|
pub code: String
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(crate = "rocket::serde")]
|
||||||
|
pub struct TotpResponseData {
|
||||||
|
#[serde(rename = "authToken")]
|
||||||
|
auth_token: String
|
||||||
|
}
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(crate = "rocket::serde")]
|
||||||
|
pub struct TotpResponseMetadata {
|
||||||
|
}
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(crate = "rocket::serde")]
|
||||||
|
pub struct TotpResponse {
|
||||||
|
data: TotpResponseData,
|
||||||
|
metadata: TotpResponseMetadata
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/v1/auth/totp", data = "<req>")]
|
||||||
|
pub async fn totp_request(req: Json<TotpRequest>, user: PartialUserInfo, db: &State<PgPool>) -> Result<(ContentType, Json<TotpResponse>), (Status, String)> {
|
||||||
|
if !match user_has_totp(user.user_id, db.inner()).await {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(e) => return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_UNABLE_TO_ISSUE", "an error occured trying to issue a session token, please try again later", e)))
|
||||||
|
} {
|
||||||
|
return Err((Status::UnprocessableEntity, TOTP_NO_TOTP_ERROR.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.has_totp_auth {
|
||||||
|
return Err((Status::BadRequest, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_TOTP_ALREADY_AUTHED", "user already has valid totp authentication")))
|
||||||
|
}
|
||||||
|
|
||||||
|
let totpmachine = match get_totpmachine(user.user_id, db.inner()).await {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(e) => {
|
||||||
|
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_UNABLE_TO_ISSUE", "an error occured trying to issue a session token, please try again later", e)))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if !totpmachine.check_current(&req.0.code).unwrap_or(false) {
|
||||||
|
return Err((Status::Unauthorized, TOTP_GENERIC_UNAUTHORIZED_ERROR.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((ContentType::JSON, Json(TotpResponse {
|
||||||
|
data: TotpResponseData { auth_token: match generate_auth_token(user.user_id as i64, user.session_id, db.inner()).await {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(e) => { return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_UNABLE_TO_ISSUE", "an error occured trying to issue a session token, please try again later", e))) }
|
||||||
|
} },
|
||||||
|
metadata: TotpResponseMetadata {},
|
||||||
|
})))
|
||||||
|
}
|
|
@ -5,7 +5,7 @@ use sqlx::PgPool;
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
use crate::auth::PartialUserInfo;
|
use crate::auth::PartialUserInfo;
|
||||||
use crate::config::TFConfig;
|
use crate::config::TFConfig;
|
||||||
use crate::tokens::create_totp_token;
|
use crate::tokens::{create_totp_token, user_has_totp};
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct TotpAuthenticatorsRequest {}
|
pub struct TotpAuthenticatorsRequest {}
|
||||||
|
@ -29,7 +29,10 @@ pub struct TotpAuthenticatorsResponse {
|
||||||
|
|
||||||
#[post("/v1/totp-authenticators", data = "<_req>")]
|
#[post("/v1/totp-authenticators", data = "<_req>")]
|
||||||
pub async fn totp_authenticators_request(_req: Json<TotpAuthenticatorsRequest>, user: PartialUserInfo, db: &State<PgPool>, config: &State<TFConfig>) -> Result<(ContentType, Json<TotpAuthenticatorsResponse>), (Status, String)> {
|
pub async fn totp_authenticators_request(_req: Json<TotpAuthenticatorsRequest>, user: PartialUserInfo, db: &State<PgPool>, config: &State<TFConfig>) -> Result<(ContentType, Json<TotpAuthenticatorsResponse>), (Status, String)> {
|
||||||
if user.has_totp {
|
if match user_has_totp(user.user_id, db.inner()).await {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(e) => return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_UNABLE_TO_ISSUE", "an error occured trying to issue a session token, please try again later", e)))
|
||||||
|
} {
|
||||||
return Err((Status::BadRequest, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_TOTP_ALREADY_EXISTS", "this user already has a totp authenticator on their account")))
|
return Err((Status::BadRequest, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_TOTP_ALREADY_EXISTS", "this user already has a totp authenticator on their account")))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
{"data":{"authToken":"auth-O7mugxdYta-RKtLMqDW4j8XCJ85EfZKKezeZZXBYtFQ"},"metadata":{}}
|
{"data":{"authToken":"auth-O7mugxdYta-RKtLMqDW4j8XCJ85EfZKKezeZZXBYtFQ"},"metadata":{}}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use std::error::Error;
|
|
||||||
use rocket::http::{ContentType, Status};
|
use rocket::http::{ContentType, Status};
|
||||||
use crate::auth::PartialUserInfo;
|
use crate::auth::PartialUserInfo;
|
||||||
use rocket::post;
|
use rocket::post;
|
||||||
|
@ -15,7 +14,6 @@ use rocket::serde::json::Json;
|
||||||
use rocket::State;
|
use rocket::State;
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use totp_rs::TOTP;
|
|
||||||
use crate::tokens::{generate_auth_token, use_totp_token, verify_totp_token};
|
use crate::tokens::{generate_auth_token, use_totp_token, verify_totp_token};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
|
|
|
@ -33,7 +33,7 @@ pub async fn generate_auth_token(user_id: i64, session_id: String, db: &PgPool)
|
||||||
Ok(token)
|
Ok(token)
|
||||||
}
|
}
|
||||||
pub async fn validate_auth_token(token: String, session_id: String, db: &PgPool) -> Result<(), Box<dyn Error>> {
|
pub async fn validate_auth_token(token: String, session_id: String, db: &PgPool) -> Result<(), Box<dyn Error>> {
|
||||||
validate_session_token(token.clone(), db).await?;
|
validate_session_token(session_id.clone(), db).await?;
|
||||||
sqlx::query!("SELECT * FROM auth_tokens WHERE id = $1 AND session_token = $2", token, session_id).fetch_one(db).await?;
|
sqlx::query!("SELECT * FROM auth_tokens WHERE id = $1 AND session_token = $2", token, session_id).fetch_one(db).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -81,3 +81,13 @@ pub async fn use_totp_token(otpid: String, email: String, db: &PgPool) -> Result
|
||||||
sqlx::query!("UPDATE users SET totp_otpurl = $1, totp_secret = $2, totp_verified = 1 WHERE email = $3", totpmachine.get_url(), totpmachine.get_secret_base32(), email).execute(db).await?;
|
sqlx::query!("UPDATE users SET totp_otpurl = $1, totp_secret = $2, totp_verified = 1 WHERE email = $3", totpmachine.get_url(), totpmachine.get_secret_base32(), email).execute(db).await?;
|
||||||
Ok(totpmachine)
|
Ok(totpmachine)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_totpmachine(user: i32, db: &PgPool) -> Result<TOTP, Box<dyn Error>> {
|
||||||
|
let user = sqlx::query!("SELECT totp_secret, totp_otpurl, email FROM users WHERE id = $1", user).fetch_one(db).await?;
|
||||||
|
let secret = Secret::Encoded(user.totp_secret);
|
||||||
|
Ok(TOTP::new(TOTP_ALGORITHM, TOTP_DIGITS, TOTP_SKEW, TOTP_STEP, secret.to_bytes().unwrap(), Some(TOTP_ISSUER.to_string()), user.email).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn user_has_totp(user: i32, db: &PgPool) -> Result<bool, Box<dyn Error>> {
|
||||||
|
Ok(sqlx::query!("SELECT totp_verified FROM users WHERE id = $1", user).fetch_one(db).await?.totp_verified == 1)
|
||||||
|
}
|
Loading…
Reference in New Issue