Skip to content

Commit

Permalink
Implemented password changing
Browse files Browse the repository at this point in the history
  • Loading branch information
themisir committed Sep 12, 2023
1 parent cb88ac8 commit f48a018
Show file tree
Hide file tree
Showing 9 changed files with 156 additions and 14 deletions.
109 changes: 99 additions & 10 deletions src/admin.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use crate::app::AppState;
use crate::auth::{Authorize, CORE_ISSUER};
use crate::auth::{Authorize, RedirectParams, TokenParams, CORE_ISSUER};
use crate::http::AppError;
use crate::store::{User, UserClaim, UserRole};

use crate::uri::UriBuilder;
use askama::Template;
use axum::extract::OriginalUri;
use axum::{
extract::{Form, Path, State},
http::{Request, StatusCode},
Expand All @@ -24,12 +26,16 @@ pub fn create_router(state: AppState) -> Router<AppState> {
.route("/users/add", post(add_user_handler))
.route("/users/:user_id/update", post(update_user_handler))
.route("/users/:user_id/claims", get(get_user_claims_page))
.route(
"/users/:user_id/create-pw-session",
get(create_user_pw_session_handler),
)
.route(
"/users/:user_id/claims/delete",
post(delete_user_claim_handler),
)
.route("/users/:user_id/claims/add", post(add_user_claim_handler))
.layer(middleware::from_fn_with_state(state, authorize))
.layer(middleware::from_fn_with_state(state, authorize_admin))
}

pub fn create_setup_router(state: AppState) -> Router<AppState> {
Expand All @@ -39,6 +45,13 @@ pub fn create_setup_router(state: AppState) -> Router<AppState> {
.layer(middleware::from_fn_with_state(state, authorize_setup))
}

pub fn create_password_change_router(state: AppState) -> Router<AppState> {
Router::new()
.route("/", get(change_password_page))
.route("/", post(change_password_handler))
.layer(middleware::from_fn_with_state(state, authorize_user))
}

async fn authorize_setup(
State(state): State<AppState>,
request: Request<Body>,
Expand All @@ -51,23 +64,99 @@ async fn authorize_setup(
})
}

async fn authorize(
async fn authorize_admin(
auth: Authorize,
OriginalUri(uri): OriginalUri,
request: Request<Body>,
next: Next<Body>,
) -> Result<Response, AppError> {
if let Some(User { role, .. }) = auth.user() {
if *role == UserRole::Admin {
return Ok(next.run(request).await);
}
}

let redirect_to = UriBuilder::new()
.set_path("/login")
.append_params(RedirectParams {
redirect_to: Some(uri.to_string()),
})
.to_string();

Ok(Redirect::to(redirect_to.as_str()).into_response())
}

async fn authorize_user(
auth: Authorize,
request: Request<Body>,
next: Next<Body>,
) -> Result<Response, AppError> {
Ok(match auth.user() {
None => StatusCode::UNAUTHORIZED.into_response(),
Some(user) => {
if user.role == UserRole::Admin {
next.run(request).await
} else {
StatusCode::FORBIDDEN.into_response()
}
}
Some(_) => next.run(request).await,
})
}

#[axum_macros::debug_handler]
async fn create_user_pw_session_handler(
State(state): State<AppState>,
Path(user_id): Path<i32>,
) -> Result<Redirect, AppError> {
let token = state
.store()
.create_user_session(user_id, CORE_ISSUER, Some(Duration::minutes(30)))
.await?;

let url = UriBuilder::new()
.set_path("/change-password")
.append_params(TokenParams { token })
.to_string();

Ok(Redirect::to(url.as_str()))
}

#[derive(Template)]
#[template(path = "password.html")]
struct ChangePasswordTemplate<'a> {
username: &'a str,
}

async fn change_password_page(authorize: Authorize) -> Result<impl IntoResponse, AppError> {
let user = authorize
.user()
.ok_or(anyhow::format_err!("unauthorized"))?;

let body = ChangePasswordTemplate {
username: user.username.as_str(),
}
.render()?;

Ok(Html(body))
}

#[derive(Deserialize)]
struct ChangePasswordDto {
password: String,
}

#[axum_macros::debug_handler]
async fn change_password_handler(
State(state): State<AppState>,
authorize: Authorize,
Form(form): Form<ChangePasswordDto>,
) -> Result<impl IntoResponse, AppError> {
let user = authorize
.user()
.ok_or(anyhow::format_err!("unauthorized"))?;

state
.store()
.change_user_password(user.id, form.password.as_str())
.await?;

Ok(Html("Password updated!"))
}

#[derive(Template)]
#[template(path = "users.html")]
struct UsersTemplate<'a> {
Expand Down
2 changes: 1 addition & 1 deletion src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ pub struct LoginRequestBody {

#[derive(Serialize, Deserialize, Debug)]
pub struct RedirectParams {
redirect_to: Option<String>,
pub redirect_to: Option<String>,
}

#[axum_macros::debug_handler]
Expand Down
6 changes: 5 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ async fn start_server(state: AppState, args: &ListenArgs) -> anyhow::Result<()>
.route("/", get(index_handler))
.route("/login", get(auth::show_login))
.route("/login", post(auth::handle_login))
.route("/logout", post(auth::logout))
.route("/logout", get(auth::logout))
.route("/authorize", get(auth::authorize))
.route("/unauthorized", get(auth::show_unauthorized))
.route(
Expand All @@ -112,6 +112,10 @@ async fn start_server(state: AppState, args: &ListenArgs) -> anyhow::Result<()>
.route("/.well-known/jwks", get(issuer::jwk_handler))
.nest("/admin", admin::create_router(state.clone()))
.nest("/setup", admin::create_setup_router(state.clone()))
.nest(
"/change-password",
admin::create_password_change_router(state.clone()),
)
.layer(middleware::from_fn_with_state(
state.clone(),
proxy::middleware,
Expand Down
1 change: 1 addition & 0 deletions src/sql/change_password.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
UPDATE users SET password_hash = ? WHERE id = ?
25 changes: 25 additions & 0 deletions src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,10 @@ impl UserStore {
password: &str,
role: UserRole,
) -> anyhow::Result<User> {
if password.is_empty() {
anyhow::bail!("password is too short")
}

let password_hash = User::create_hash(password.as_bytes())
.map_err(|err| anyhow::format_err!("failed to create hash: {}", err))?;
let (username, normalized_username) = Self::normalize_username(username);
Expand Down Expand Up @@ -226,6 +230,27 @@ impl UserStore {
})
}

pub async fn change_user_password(&self, user_id: i32, password: &str) -> anyhow::Result<()> {
if password.is_empty() {
anyhow::bail!("password is too short")
}

let password_hash = User::create_hash(password.as_bytes())
.map_err(|err| anyhow::format_err!("failed to create hash: {}", err))?;

let row = sqlx::query(include_str!("sql/change_password.sql"))
.bind(password_hash)
.bind(user_id)
.execute(self.get_pool())
.await?;

match row.rows_affected() {
1 => Ok(()),
0 => anyhow::bail!("password has not changed"),
_ => anyhow::bail!("we fucked up"),
}
}

pub async fn find_user_by_session(
&self,
session_id: &str,
Expand Down
2 changes: 1 addition & 1 deletion src/templates/add-user.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
{% block title %}Create user{% endblock %}

{% block content %}
<h1>Create user account</h1>
<h1><a href="/admin/users">Users</a> / Create account</h1>

<form method="post">
<div>
Expand Down
2 changes: 1 addition & 1 deletion src/templates/claims.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
{% block title %}Claims{% endblock %}

{% block content %}
<h1>User claims</h1>
<h1><a href="/admin/users">Users</a> / Claims</h1>

<table>
<thead>
Expand Down
22 changes: 22 additions & 0 deletions src/templates/password.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{% extends "base.html" %}

{% block title %}Change password{% endblock %}

{% block content %}
<h1>Change password</h1>

<form method="post">
<div>
<label for="username">Username:</label>
<input id="username" type="text" disabled value="{{ username|e }}" />
</div>

<div>
<label for="password">New password:</label>
<input id="password" name="password" type="password" />
</div>

<button>Update</button>
</form>

{% endblock %}
1 change: 1 addition & 0 deletions src/templates/users.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ <h1>Users</h1>
<td>{{ user.role|e }}</td>
<td class="actions">
<a href="/admin/users/{{ user.id|e }}/claims">Claims</a>
<a href="/admin/users/{{ user.id|e }}/create-pw-session">Password</a>
{% if user.role == crate::store::UserRole::Admin %}
<a data-submit="/admin/users/{{ user.id|e }}/update"
data-submit-confirm="Are you sure?"
Expand Down

0 comments on commit f48a018

Please sign in to comment.