Skip to content

Commit

Permalink
refactor: Removed javascript from form submission
Browse files Browse the repository at this point in the history
  • Loading branch information
justinrubek committed Sep 2, 2023
1 parent 9a0b993 commit 182ef56
Show file tree
Hide file tree
Showing 2 changed files with 134 additions and 45 deletions.
134 changes: 128 additions & 6 deletions crates/http/src/handlers/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ use argon2::{
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
Argon2,
};
use axum::extract::State;
use axum::{
extract::State,
http::HeaderValue,
response::{IntoResponse, Response},
Form,
};
use hyper::{header, StatusCode};
use jsonwebtoken::EncodingKey;
use lockpad_auth::Claims;
use lockpad_models::{api_key::ApiKey, entity::Builder, user::User};
Expand Down Expand Up @@ -39,6 +45,12 @@ pub(crate) struct AuthorizeResponse {
token: String,
}

#[derive(Debug, Deserialize)]
pub(crate) struct ApplicationParams {
pub redirect_uri: String,
pub client_id: String,
}

/// Hashes a string using argon2.
/// This is performed on any password before it is stored in the database.
pub(crate) async fn hash_string(data: &[u8]) -> Result<String> {
Expand Down Expand Up @@ -70,14 +82,14 @@ pub(crate) async fn register(
pg_pool,
..
}): State<ServerState>,
payload: axum::extract::Json<UserCredentials>,
) -> Result<axum::response::Json<AuthorizeResponse>> {
Form(payload): Form<UserCredentials>,
) -> Result<impl IntoResponse> {
// TODO: Check against database to see if the username is already taken.

let password_hash = hash_string(&payload.0.password.into_bytes()).await?;
let password_hash = hash_string(&payload.password.into_bytes()).await?;

let user = User::builder()
.identifier(payload.0.username)
.identifier(payload.username)
.secret(password_hash)
.build()?;

Expand All @@ -88,13 +100,38 @@ pub(crate) async fn register(
let token = Claims::new(user_id).encode(&encoding_key).await?;

// for now, return a dummy token
Ok(axum::response::Json(AuthorizeResponse { token }))
Ok(Redirect::found(&format!(
"http://localhost:4000/?token={token}"
)))
}

/// Performs the authorization process.
/// This is where the user's credentials are checked against the database.
/// If the credentials are valid, a token is generated and sent to the user.
pub(crate) async fn authorize(
State(ServerState {
encoding_key,
pg_pool,
..
}): State<ServerState>,
Form(payload): Form<Credentials>,
) -> Result<impl IntoResponse> {
// If we're here, the user is authorized.
match payload {
Credentials::User(_) => {
let token = payload.authorize(&encoding_key, &pg_pool).await?;
Ok(Redirect::found(&format!(
"http://localhost:4000/?token={token}"
)))
}
Credentials::ApiKey(_) => {
todo!()
}
}
}

/// Performs the authorization process, but with JSON request bodies.
pub(crate) async fn authorize_json(
State(ServerState {
encoding_key,
pg_pool,
Expand Down Expand Up @@ -164,3 +201,88 @@ async fn authorize_api_key(
}
}
}

impl Credentials {
async fn authorize(self, encoding_key: &EncodingKey, pg_pool: &sqlx::PgPool) -> Result<String> {
match self {
Credentials::User(payload) => {
let user = User::by_identifier(pg_pool, &payload.username).await?;

match user {
None => {
tracing::debug!("user not found");

Err(Error::Unauthorized)
}
Some(user) => {
tracing::debug!(?user.user_id, "user found");
tracing::debug!(?user.user_id, "user found");

validate_hash(payload.password.as_bytes(), &user.secret).await?;
tracing::debug!("password verified");

let token = Claims::new(user.user_id.to_string())
.encode(encoding_key)
.await?;
Ok(token)
}
}
}
Credentials::ApiKey(payload) => {
let api_key =
Ulid::from_str(&payload.api_key_id).map_err(|_| Error::Unauthorized)?;
let api_key = ApiKey::by_id(pg_pool, &api_key).await?;

match api_key {
None => {
tracing::debug!("user not found");

Err(Error::Unauthorized)
}
Some(api_key) => {
let owner_id = api_key.owner_id.to_string();

tracing::debug!(owner_id, "user found");

validate_hash(payload.api_secret.as_bytes(), &api_key.secret).await?;

let token = Claims::new(owner_id.to_string())
.encode(encoding_key)
.await?;
Ok(token)
}
}
}
}
}
}

// TODO: Get this upstreamed
struct Redirect {
status_code: StatusCode,
location: HeaderValue,
}

impl Redirect {
fn found(uri: &str) -> Self {
Self::with_status_code(StatusCode::FOUND, uri)
}

fn with_status_code(status_code: StatusCode, uri: &str) -> Self {
assert!(
status_code.is_redirection(),
"not a redirection status code"
);

Self {
status_code,
location: HeaderValue::try_from(uri).expect("URI isn't a valid header value"),
}
}
}

impl IntoResponse for Redirect {
fn into_response(self) -> Response {
(self.status_code, [(header::LOCATION, self.location)]).into_response()
}
}
45 changes: 6 additions & 39 deletions crates/http/src/handlers/pages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,61 +233,28 @@ impl std::string::ToString for HtmlForm {
HtmlFormType::Login => "Login",
};

let submit_uri = &self.submit_uri;
let form_name = format!("{}-form", type_name);

let form_html = format!(
r#"
<form id="{form_name}">
<form
id="{form_name}"
action="{submit_uri}"
method="POST"
>
<input type="text" id="username" name="username" placeholder="username" />
<input type="password" id="password" name="password" placeholder="password" />
<input type="submit" value="{type_display}" />
</form>
"#,
);

let submit_uri = &self.submit_uri;

let script_html = format!(
r#"
<script>
const form = document.getElementById("{form_name}");
form.onsubmit = function(event) {{
console.log("submitting form");
event.preventDefault();
const data = new FormData(form);
const username = data.get("username");
const password = data.get("password");
// Perform a POST request to /authorize
// If the request is successful, the s
fetch("{submit_uri}", {{
method: "POST",
body: JSON.stringify({{ username, password }}),
headers: {{
"Content-Type": "application/json",
}},
}})
.then(response => response.json())
.then(data => {{
console.log("Success:", data);
}})
.catch((error) => {{
console.error("Error:", error);
}});
}}
</script>
"#
);

format!(
r#"
<h1>{type_display}</h1>
{form_html}
{script_html}
"#,
type_display = type_display,
form_html = form_html,
script_html = script_html,
)
}
}

0 comments on commit 182ef56

Please sign in to comment.