totp: added page to verify totp token
This commit is contained in:
parent
1e42208e6b
commit
fbbcb4e182
4 changed files with 234 additions and 0 deletions
|
|
@ -28,6 +28,7 @@ pub enum Page {
|
||||||
ForgotPassword,
|
ForgotPassword,
|
||||||
ResetPassword(ResetPassword),
|
ResetPassword(ResetPassword),
|
||||||
UserSecurityTotp(UserSecurityTotp),
|
UserSecurityTotp(UserSecurityTotp),
|
||||||
|
AuthorizeTotp(AuthorizeTotp),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Page {
|
impl Page {
|
||||||
|
|
@ -52,6 +53,7 @@ impl Page {
|
||||||
Page::ForgotPassword => "pages/forgot-password",
|
Page::ForgotPassword => "pages/forgot-password",
|
||||||
Page::ResetPassword(_) => "pages/reset-password",
|
Page::ResetPassword(_) => "pages/reset-password",
|
||||||
Page::UserSecurityTotp(_) => "pages/settings/totp",
|
Page::UserSecurityTotp(_) => "pages/settings/totp",
|
||||||
|
Page::AuthorizeTotp(_) => "pages/oauth/totp",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,6 +78,7 @@ impl Page {
|
||||||
Page::ForgotPassword => "Forgot password",
|
Page::ForgotPassword => "Forgot password",
|
||||||
Page::ResetPassword(_) => "Reset password",
|
Page::ResetPassword(_) => "Reset password",
|
||||||
Page::UserSecurityTotp(_) => "Enable One-time password",
|
Page::UserSecurityTotp(_) => "Enable One-time password",
|
||||||
|
Page::AuthorizeTotp(_) => "Verifying your account",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,6 +105,7 @@ impl Page {
|
||||||
Page::ForgotPassword => None,
|
Page::ForgotPassword => None,
|
||||||
Page::ResetPassword(_) => None,
|
Page::ResetPassword(_) => None,
|
||||||
Page::UserSecurityTotp(_) => Some(UserMenu::Settings.into()),
|
Page::UserSecurityTotp(_) => Some(UserMenu::Settings.into()),
|
||||||
|
Page::AuthorizeTotp(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -126,6 +130,7 @@ impl Page {
|
||||||
Page::ForgotPassword => Box::new(()),
|
Page::ForgotPassword => Box::new(()),
|
||||||
Page::ResetPassword(reset) => Box::new(reset),
|
Page::ResetPassword(reset) => Box::new(reset),
|
||||||
Page::UserSecurityTotp(totp) => Box::new(totp),
|
Page::UserSecurityTotp(totp) => Box::new(totp),
|
||||||
|
Page::AuthorizeTotp(totp) => Box::new(totp),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use self::totp::*;
|
||||||
use authorize::*;
|
use authorize::*;
|
||||||
use redirect::*;
|
use redirect::*;
|
||||||
use rocket::{routes, Route};
|
use rocket::{routes, Route};
|
||||||
|
|
@ -7,6 +8,7 @@ use userinfo::*;
|
||||||
pub mod authorize;
|
pub mod authorize;
|
||||||
pub mod redirect;
|
pub mod redirect;
|
||||||
pub mod token;
|
pub mod token;
|
||||||
|
pub mod totp;
|
||||||
pub mod userinfo;
|
pub mod userinfo;
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
|
|
@ -17,6 +19,8 @@ pub fn routes() -> Vec<Route> {
|
||||||
redirect_page,
|
redirect_page,
|
||||||
request_token,
|
request_token,
|
||||||
get_userinfo,
|
get_userinfo,
|
||||||
|
totp_page,
|
||||||
|
totp_verify,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -42,4 +46,12 @@ pub mod content {
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub home_page: String,
|
pub home_page: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(crate = "rocket::serde")]
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AuthorizeTotp {
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub username: String,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
143
crates/ezidam/src/routes/oauth/totp.rs
Normal file
143
crates/ezidam/src/routes/oauth/totp.rs
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
use crate::routes::oauth::{redirect_uri, AUTHORIZATION_CODE_LEN};
|
||||||
|
use crate::routes::prelude::*;
|
||||||
|
use apps::App;
|
||||||
|
use authorization_codes::AuthorizationCode;
|
||||||
|
use hash::SecretString;
|
||||||
|
use rocket::http::{Cookie, CookieJar};
|
||||||
|
use rocket::{get, post};
|
||||||
|
use users::totp_login_request::TOTP_REQUEST_COOKIE_NAME;
|
||||||
|
use users::User;
|
||||||
|
|
||||||
|
#[get("/oauth/totp?<auth_request..>")]
|
||||||
|
pub async fn totp_page(
|
||||||
|
totp_request: TotpRequest,
|
||||||
|
mut db: Connection<Database>,
|
||||||
|
flash: Option<FlashMessage<'_>>,
|
||||||
|
auth_request: AuthenticationRequest<'_>,
|
||||||
|
) -> Result<Template> {
|
||||||
|
let mut transaction = db.begin().await?;
|
||||||
|
|
||||||
|
// Get app info
|
||||||
|
let _app = App::get_valid_app(
|
||||||
|
&mut transaction,
|
||||||
|
auth_request.response_type,
|
||||||
|
auth_request.scope,
|
||||||
|
auth_request.client_id,
|
||||||
|
auth_request.redirect_uri,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Get totp request
|
||||||
|
let totp_request =
|
||||||
|
users::totp_login_request::TotpLoginRequest::get_one(&mut transaction, &totp_request.0)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| Error::not_found("Failed to find totp request"))?;
|
||||||
|
|
||||||
|
if totp_request.has_expired() {
|
||||||
|
return Err(Error::bad_request("Totp request has expired"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if totp_request.used_at().is_some() {
|
||||||
|
return Err(Error::bad_request("Totp request has been used"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user
|
||||||
|
let user = User::get_by_id(&mut transaction, totp_request.user())
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| Error::not_found("Failed to find user"))?;
|
||||||
|
|
||||||
|
transaction.commit().await?;
|
||||||
|
|
||||||
|
let page = Page::AuthorizeTotp(super::content::AuthorizeTotp {
|
||||||
|
name: user.name().map(|name| name.to_string()),
|
||||||
|
username: user.username().into(),
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(flash
|
||||||
|
.map(|flash| Page::with_flash(page.clone(), flash))
|
||||||
|
.unwrap_or_else(|| page.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, FromForm)]
|
||||||
|
pub struct TotpVerifyForm<'r> {
|
||||||
|
pub code: &'r str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/oauth/totp?<auth_request..>", data = "<form>")]
|
||||||
|
pub async fn totp_verify(
|
||||||
|
totp_request: TotpRequest,
|
||||||
|
form: Form<TotpVerifyForm<'_>>,
|
||||||
|
mut db: Connection<Database>,
|
||||||
|
auth_request: AuthenticationRequest<'_>,
|
||||||
|
cookie_jar: &CookieJar<'_>,
|
||||||
|
) -> Result<Either<Redirect, Flash<Redirect>>> {
|
||||||
|
let mut transaction = db.begin().await?;
|
||||||
|
|
||||||
|
// Get app info
|
||||||
|
let app = App::get_valid_app(
|
||||||
|
&mut transaction,
|
||||||
|
auth_request.response_type,
|
||||||
|
auth_request.scope,
|
||||||
|
auth_request.client_id,
|
||||||
|
auth_request.redirect_uri,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Get totp request
|
||||||
|
let totp_request =
|
||||||
|
users::totp_login_request::TotpLoginRequest::get_one(&mut transaction, &totp_request.0)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| Error::not_found("Failed to find totp request"))?;
|
||||||
|
|
||||||
|
if totp_request.has_expired() {
|
||||||
|
return Err(Error::bad_request("Totp request has expired"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if totp_request.used_at().is_some() {
|
||||||
|
return Err(Error::bad_request("Totp request has been used"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user
|
||||||
|
let user = User::get_by_id(&mut transaction, totp_request.user())
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| Error::not_found("Failed to find user"))?;
|
||||||
|
|
||||||
|
transaction.commit().await?;
|
||||||
|
|
||||||
|
let totp_secret = user
|
||||||
|
.totp_secret()
|
||||||
|
.ok_or_else(|| Error::bad_request("TOTP is not enabled for user"))?;
|
||||||
|
|
||||||
|
// Create totp
|
||||||
|
let totp = totp::new(totp_secret, None, user.username().to_string())?;
|
||||||
|
|
||||||
|
// Verify totp code
|
||||||
|
if !totp.check_current(form.code)? {
|
||||||
|
return Ok(Either::Right(Flash::new(
|
||||||
|
Redirect::to(uri!(totp_page(auth_request))),
|
||||||
|
FlashKind::Danger,
|
||||||
|
"Wrong code. Please try again.",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate authorization code
|
||||||
|
let code = task::spawn_blocking(|| SecretString::new(AUTHORIZATION_CODE_LEN)).await?;
|
||||||
|
|
||||||
|
let mut transaction = db.begin().await?;
|
||||||
|
|
||||||
|
// Save authorization code
|
||||||
|
AuthorizationCode::insert(&mut transaction, code.as_ref(), app.id(), user.id()).await?;
|
||||||
|
|
||||||
|
// Mark totp token as used
|
||||||
|
totp_request.use_code(&mut transaction).await?;
|
||||||
|
|
||||||
|
transaction.commit().await?;
|
||||||
|
|
||||||
|
// Delete cookie
|
||||||
|
cookie_jar.remove(Cookie::named(TOTP_REQUEST_COOKIE_NAME));
|
||||||
|
|
||||||
|
// Construct uri to redirect to
|
||||||
|
let uri = redirect_uri(auth_request, &app, &code);
|
||||||
|
|
||||||
|
Ok(Either::Left(Redirect::found(uri)))
|
||||||
|
}
|
||||||
74
crates/ezidam/templates/pages/oauth/totp.html.tera
Normal file
74
crates/ezidam/templates/pages/oauth/totp.html.tera
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
{% extends "base" %}
|
||||||
|
|
||||||
|
{% import "utils/user" as user %}
|
||||||
|
{% import "utils/form" as form %}
|
||||||
|
|
||||||
|
{% block page %}
|
||||||
|
<body class=" d-flex flex-column">
|
||||||
|
<script src="/js/demo-theme.min.js"></script>
|
||||||
|
<div>
|
||||||
|
<div class="min-vh-100 d-flex flex-column justify-content-between">
|
||||||
|
<div class="container container-tight py-4">
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
{% include "utils/logo" %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if flash %}
|
||||||
|
<div class="alert alert-{{flash.0}}" role="alert">
|
||||||
|
<h4 class="alert-title">{{ flash.1 }}</h4>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="card card-md">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<div class="mb-4">
|
||||||
|
<h2 class="card-title">Verify your account</h2>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
{{ user::avatar(username=username, name=name, size="lg", css="mb-3") }}
|
||||||
|
<h3>
|
||||||
|
{% if name %}
|
||||||
|
{{ name }}
|
||||||
|
{% else %}
|
||||||
|
{{ username }}
|
||||||
|
{% endif %}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form id="totp_form" action="" method="post" autocomplete="off" novalidate class="mt-4">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label required" for="code">
|
||||||
|
Enter the code displayed on your device
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
type="text"
|
||||||
|
name="code"
|
||||||
|
id="code"
|
||||||
|
inputmode="numeric"
|
||||||
|
pattern="[0-9]*"
|
||||||
|
autocomplete="one-time-code"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-footer">
|
||||||
|
<button type="submit" class="btn btn-primary w-100">Verify code</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% include "shell/footer" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Libs JS -->
|
||||||
|
<!-- Tabler Core -->
|
||||||
|
<script src="/js/tabler.min.js" defer></script>
|
||||||
|
<script src="/js/demo.min.js" defer></script>
|
||||||
|
|
||||||
|
{{ form::disable_button_delay_submit(form_id="totp_form") }}
|
||||||
|
|
||||||
|
</body>
|
||||||
|
{% endblock page %}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue