Skip to content

Commit

Permalink
make login work
Browse files Browse the repository at this point in the history
code is very dirty for now,
to be cleaned up later, for now
we just care about functionality
  • Loading branch information
glendc committed Oct 22, 2023
1 parent a85e5cc commit 5e343ed
Show file tree
Hide file tree
Showing 11 changed files with 215 additions and 12 deletions.
29 changes: 29 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@ shuttle-runtime = "0.29"
shuttle-secrets = "0.29"
tokio = "1.28"
tower = { version = "0.4", features = ["tracing"] }
tower-cookies = "0.9"
tower-http = { version = "0.4", features = ["fs", "trace", "compression-full", "normalize-path"] }
tracing = "0.1"
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ async fn axum(#[shuttle_secrets::Secrets] secret_store: SecretStore) -> shuttle_
let state = router::State { auth };
let router = router::new(state);

tracing::debug!("starting axum router");
Ok(router.into())
}
18 changes: 15 additions & 3 deletions src/router/index.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
use std::sync::Arc;

use askama::Template;
use askama_axum::{IntoResponse, Response};
use axum::extract::State;
use tower_cookies::Cookies;

#[derive(Template)]
#[template(path = "../templates/index.html")]
pub struct GetTemplate;
pub struct GetTemplate {
pub email: String,
}

#[derive(Template)]
#[template(path = "../templates/content/login.html")]
pub struct IndexLoginTemplate;

pub async fn get() -> IndexLoginTemplate {
IndexLoginTemplate
pub async fn get(State(state): State<Arc<crate::router::State>>, cookies: Cookies) -> Response {
if let Some(cookie) = cookies.get(crate::services::COOKIE_NAME) {
if let Some(email) = state.auth.verify_cookie(cookie.value()) {
return GetTemplate { email }.into_response();
}
}
IndexLoginTemplate.into_response()
}
8 changes: 6 additions & 2 deletions src/router/link.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ use serde::Deserialize;

#[derive(Template)]
#[template(path = "../templates/index.html")]
pub struct GetTemplate;
pub struct GetTemplate {
pub email: String,
}

pub async fn get() -> GetTemplate {
GetTemplate
GetTemplate {
email: "[email protected]".to_string(),
}
}

#[derive(Template)]
Expand Down
27 changes: 25 additions & 2 deletions src/router/login.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,42 @@ use axum::{
Form,
};
use serde::Deserialize;
use tower_cookies::{cookie::time::OffsetDateTime, Cookie, Cookies};

#[derive(Deserialize)]
pub struct GetQuery {
pub magic: Option<String>,
}

pub async fn get(Query(query): Query<GetQuery>) -> Redirect {
pub async fn get(
Query(query): Query<GetQuery>,
State(state): State<Arc<crate::router::State>>,
cookies: Cookies,
) -> Redirect {
let magic = match query.magic {
Some(magic) => magic,
None => return Redirect::temporary("/"),
};

println!("Login attempt with magic link: {}", magic);
match state.auth.verify_magic(magic) {
Some((magic, expires_at)) => {
let mut cookie = Cookie::new(crate::services::COOKIE_NAME, magic);
cookie.set_path("/");
let offset = OffsetDateTime::from_unix_timestamp(expires_at as i64).unwrap();
cookie.set_expires(offset);
cookies.add(cookie);
}
None => {
let mut cookie = Cookie::new(crate::services::COOKIE_NAME, "");
cookie.set_path("/");
let offset = OffsetDateTime::from_unix_timestamp(0_i64).unwrap();
cookie.set_expires(offset);
cookies.add(cookie);
return Redirect::to("/");
}
}

tracing::debug!("login user with magic link");
Redirect::temporary("/")
}

Expand Down
19 changes: 19 additions & 0 deletions src/router/logout.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use axum::response::Redirect;
use serde::Deserialize;
use tower_cookies::{cookie::time::OffsetDateTime, Cookie, Cookies};

#[derive(Deserialize)]
pub struct GetQuery {
pub magic: Option<String>,
}

pub async fn get(cookies: Cookies) -> Redirect {
let mut cookie = Cookie::new(crate::services::COOKIE_NAME, "");
cookie.set_path("/");
let offset = OffsetDateTime::from_unix_timestamp(0_i64).unwrap();
cookie.set_expires(offset);
cookies.add(cookie);

tracing::debug!("logout user upon their request");
Redirect::temporary("/")
}
4 changes: 4 additions & 0 deletions src/router/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use axum::{
Router,
};
use tower::ServiceBuilder;
use tower_cookies::CookieManagerLayer;
use tower_http::{
compression::CompressionLayer, normalize_path::NormalizePathLayer, services::ServeDir,
trace::TraceLayer,
Expand All @@ -13,6 +14,7 @@ use tower_http::{
mod index;
mod link;
mod login;
mod logout;
mod memory;
mod not_found;
mod redirect;
Expand All @@ -32,8 +34,10 @@ fn new_root(state: State) -> Router {
.route("/link", post(link::post))
.route("/login", get(login::get))
.route("/login", post(login::post))
.route("/logout", get(logout::get))
.route("/:hash", get(redirect::get))
.with_state(Arc::new(state))
.layer(CookieManagerLayer::new())
}

pub fn new(state: State) -> Router {
Expand Down
113 changes: 110 additions & 3 deletions src/services/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use axum::http::StatusCode;
use base64::{engine::general_purpose, Engine as _};
use orion::aead::SecretKey;

pub const COOKIE_NAME: &str = "bckt-auth";

#[derive(Debug)]
pub struct Auth {
email_validator: EmailValidator,
Expand Down Expand Up @@ -48,7 +50,7 @@ impl Auth {
StatusCode::INTERNAL_SERVER_ERROR,
)
})?;
let magic = general_purpose::STANDARD_NO_PAD.encode(&cipher_text);
let magic = general_purpose::URL_SAFE.encode(&cipher_text);

// send magic
let client = reqwest::Client::new();
Expand Down Expand Up @@ -125,12 +127,109 @@ impl Auth {

Ok(())
}

pub fn verify_magic(&self, magic: impl AsRef<str>) -> Option<(String, u64)> {
let magic = magic.as_ref();
let cipher_text = match general_purpose::URL_SAFE.decode(magic.as_bytes()) {
Ok(cipher_text) => cipher_text,
Err(e) => {
tracing::debug!("failed decoding magic: {:?}", e);
return None;
}
};
let magic = match orion::aead::open(&self.secret_key, &cipher_text) {
Ok(magic) => magic,
Err(e) => {
tracing::debug!("failed decrypting magic: {:?}", e);
return None;
}
};
let mut magic = match AuthTokenMagic::try_from(std::str::from_utf8(&magic).unwrap()) {
Ok(magic) => magic,
Err(e) => {
tracing::debug!("failed parsing magic: {:?}", e);
return None;
}
};
if magic.expires_at < chrono::Utc::now().timestamp() as u64 {
tracing::debug!("magic expired");
return None;
}
if magic.verified {
tracing::debug!("magic already verified");
return None;
}

// make it verified and allow it to be used for a week
magic.verified = true;
magic.expires_at = chrono::Utc::now()
.checked_add_signed(chrono::Duration::days(7))
.unwrap()
.timestamp() as u64;
let expires_at = magic.expires_at;

let magic = magic.to_string();
let result_cipher_text =
orion::aead::seal(&self.secret_key, magic.as_bytes()).map_err(|e| {
tracing::error!("failed encrypting magic: {:?}", e);
(
"failed encrypting magic".to_string(),
StatusCode::INTERNAL_SERVER_ERROR,
)
});
let cipher_text = match result_cipher_text {
Ok(cipher_text) => cipher_text,
Err(e) => {
tracing::debug!("failed encrypting magic: {:?}", e);
return None;
}
};
let magic = general_purpose::URL_SAFE.encode(cipher_text);

Some((magic, expires_at))
}

pub fn verify_cookie(&self, magic: impl AsRef<str>) -> Option<String> {
let magic = magic.as_ref();
let cipher_text = match general_purpose::URL_SAFE.decode(magic.as_bytes()) {
Ok(cipher_text) => cipher_text,
Err(e) => {
tracing::debug!("failed decoding magic: {:?}", e);
return None;
}
};
let magic = match orion::aead::open(&self.secret_key, &cipher_text) {
Ok(magic) => magic,
Err(e) => {
tracing::debug!("failed decrypting magic: {:?}", e);
return None;
}
};
let magic = match AuthTokenMagic::try_from(std::str::from_utf8(&magic).unwrap()) {
Ok(magic) => magic,
Err(e) => {
tracing::debug!("failed parsing magic: {:?}", e);
return None;
}
};
if magic.expires_at < chrono::Utc::now().timestamp() as u64 {
tracing::debug!("magic expired");
return None;
}
if !magic.verified {
tracing::debug!("magic not yet verified");
return None;
}

Some(magic.email)
}
}

struct AuthTokenMagic {
email: String,
token: Vec<u8>,
expires_at: u64,
verified: bool,
}

impl AuthTokenMagic {
Expand All @@ -145,6 +244,7 @@ impl AuthTokenMagic {
email,
token: token.to_vec(),
expires_at,
verified: false,
})
}
}
Expand All @@ -153,8 +253,9 @@ impl std::fmt::Display for AuthTokenMagic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let value = serde_json::json!({
"email": self.email,
"token": general_purpose::STANDARD_NO_PAD.encode(&self.token),
"token": general_purpose::URL_SAFE.encode(&self.token),
"expires_at": self.expires_at,
"verified": self.verified,
})
.to_string();
write!(f, "{value}")
Expand All @@ -177,18 +278,24 @@ impl TryFrom<&str> for AuthTokenMagic {
.ok_or("missing token")?
.as_str()
.ok_or("invalid token")?;
let token = general_purpose::STANDARD_NO_PAD
let token = general_purpose::URL_SAFE
.decode(token.as_bytes())
.map_err(|e| e.to_string())?;
let expires_at = value
.get("expires_at")
.ok_or("missing expires_at")?
.as_u64()
.ok_or("invalid expires_at")?;
let verified = value
.get("verified")
.ok_or("missing verified")?
.as_bool()
.ok_or("invalid verified")?;
Ok(Self {
email,
token,
expires_at,
verified,
})
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/services/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
mod auth;
pub use auth::Auth;
pub use auth::{Auth, COOKIE_NAME};
5 changes: 4 additions & 1 deletion templates/index.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
{% extends "base.html" %}
{% block content %}
<div class="container">
<h1>Hello!</h1>
<h1 style="margin: 0">Welcome <code>{{ email }}</code>.</h1>
<section class="tool-bar">
<a href="/logout" class="<button>">Logout 👋</a>
</section>
</div>
<div class="box card">
<strong class="block titlebar">🔗 Create a Shortlink</strong>
Expand Down

0 comments on commit 5e343ed

Please sign in to comment.