Marc Hauschildt
Web Technologies and Computer Software Development Instructor at Kirkwood Community College in Cedar Rapids, IA.
Week 5
CREATE TABLE password_reset (
id INT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL,
token VARCHAR(255) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (email) REFERENCES user(email) ON DELETE CASCADE
);
CREATE PROCEDURE sp_add_password_reset(
IN p_email VARCHAR(255),
IN p_token VARCHAR(255)
)
BEGIN
-- Delete any previous password_reset
DELETE FROM password_reset WHERE email = p_email;
-- Create a new password_reset
INSERT INTO password_reset (email, token) VALUES (p_email, p_token);
END;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/reset-password")
public class ResetPassword extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setAttribute("pageTitle", "Reset your password");
req.getRequestDispatcher("WEB-INF/reset-password.jsp").forward(req, resp);
}
}
<main>
<section class="p-0 d-flex align-items-center position-relative overflow-hidden">
<div class="container-fluid">
<div class="row">
<div class="col-12 col-lg-8 m-auto">
<div class="row my-5">
<div class="col-sm-10 col-xl-8 m-auto">
<!-- Title -->
<h1 class="fs-2">Reset Password</h1>
<p class="lead mb-4">Enter your email address to reset your password.</p>
<c:if test="${not empty passwordResetMsg}">
<div class="alert alert-warning mb-2" role="alert">
${passwordResetMsg}
</div>
</c:if>
<!-- Form START -->
<form method="post" action="${appURL}/reset-password">
<!-- Email -->
<div class="mb-4">
<label for="inputEmail" class="form-label">Email address *</label>
<div class="input-group input-group-lg">
<span class="input-group-text bg-light rounded-start border-0 text-secondary px-3"><i class="bi bi-envelope-fill"></i></span>
<input type="text" class="form-control border-0 bg-light rounded-end ps-1" placeholder="E-mail" id="inputEmail" name="inputEmail" value="${email}">
</div>
</div>
<!-- Button -->
<div class="align-items-center mt-0">
<div class="d-grid">
<button class="btn btn-primary mb-0" type="submit">Submit</button>
</div>
</div>
</form>
<!-- Form END -->
<!-- Sign in link -->
<div class="mt-4 text-center">
<span><a href="${appURL}/login">Login</a></span>
</div>
</div>
</div> <!-- Row END -->
</div>
</div> <!-- Row END -->
</div>
</section>
</main>
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String email = req.getParameter("inputEmail");
req.setAttribute("email", email);
String message = UserDAO.passwordReset(email, req);
req.setAttribute("passwordResetMsg", message);
req.setAttribute("pageTitle", "Reset your password");
req.getRequestDispatcher("WEB-INF/reset-password.jsp").forward(req, resp);
}
<p class="my-3 text-body-secondary"><a href="${appURL}/reset-password">Forgot password?</a><br>Don't have an account? <a href="${appURL}/signup">Sign-up</a></p>
public static String passwordReset(String email, HttpServletRequest req) {
User user = get(email);
if (user == null) {
return "No user found that matches the email";
} else {
try (Connection connection = getConnection()) {
if (connection != null) {
String uuid = String.valueOf(UUID.randomUUID());
try (CallableStatement statement = connection.prepareCall("{CALL sp_add_password_reset(?, ?)}")) {
statement.setString(1, email);
statement.setString(2, uuid);
statement.executeUpdate();
}
String subject = "Reset Password";
String message = "<h2Reset Password</h2>";
message += "<p>Please use this link to securely reset your password. This link will remain active for 30 minutes.</p>";
String appUrl = "";
if(req.isSecure()) {
appUrl = req.getServletContext().getInitParameter("appURLAzure");
} else {
appUrl = req.getServletContext().getInitParameter("appURLLocal");
}
String fullURL = String.format("%s/new-password?key=%s", appUrl, uuid);
message += String.format("<p><a href=\"%s\" target=\"_blank\">%s</a></p>", fullURL, fullURL);
message += "<p>If you did not request to reset your password, you can ignore this message and your password will not be changed.</p>";
// Send Email
return "If there's an account associated with the email entered, we will send a password reset link.";
}
} catch (SQLException e) {
return "Error resetting password";
}
}
return "Error - Could not send password reset email";
}
import com.azure.communication.email.EmailClient;
import com.azure.communication.email.EmailClientBuilder;
import io.github.cdimascio.dotenv.Dotenv;
import com.azure.communication.email.models.EmailAddress;
import com.azure.communication.email.models.EmailMessage;
import com.azure.communication.email.models.EmailSendResult;
import com.azure.core.util.polling.PollResponse;
import com.azure.core.util.polling.SyncPoller;
public class AzureEmail {
public static EmailClient getEmailClient() {
String connectionString = Dotenv.load().get("AZURE_EMAIL_CONNECTION");
EmailClient emailClient = new EmailClientBuilder()
.connectionString(connectionString)
.buildClient();
return emailClient;
}
public static String sendEmail(String toEmailAddress, String subject, String bodyHTML) {
EmailClient emailClient = getEmailClient();
EmailAddress toAddress = new EmailAddress(toEmailAddress);
String body = Helpers.html2text(bodyHTML);
EmailMessage emailMessage = new EmailMessage()
.setSenderAddress(Dotenv.load().get("AZURE_EMAIL_FROM"))
.setToRecipients(toAddress)
.setSubject(subject)
.setBodyPlainText(body)
.setBodyHtml(bodyHTML);
SyncPoller<EmailSendResult, EmailSendResult> poller = null;
try {
poller = emailClient.beginSend(emailMessage, null);
} catch(RuntimeException e) {
return e.getMessage();
}
PollResponse<EmailSendResult> result = poller.waitForCompletion();
return "";
}
}
import org.jsoup.Jsoup;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.DecimalFormat;
public class Helpers {
public static String round(double number, int numDecPlaces) {
BigDecimal bigDecimal = new BigDecimal(Double.toString(number));
bigDecimal = bigDecimal.setScale(numDecPlaces, RoundingMode.HALF_UP).stripTrailingZeros();
return bigDecimal.toString();
}
// https://stackoverflow.com/a/3149645/6629315
public static String html2text(String html) {
return Jsoup.parse(html).text();
}
}
public class EmailThread extends Thread {
private String toEmailAddress;
private String subject;
private String bodyHTML;
private String errorMessage;
public EmailThread(String toEmailAddress, String subject, String bodyHTML) {
this.toEmailAddress = toEmailAddress;
this.subject = subject;
this.bodyHTML = bodyHTML;
}
public void run() {
errorMessage = AzureEmail.sendEmail(toEmailAddress, subject, bodyHTML);
// TODO: Add a backup email service if an error occurs
}
public String getErrorMessage() {
return errorMessage;
}
}
public static void main(String[] args) {
String email = "abc@example.com"; // Use your own email address NOT THE TEACHER'S
String subject = "Testing";
String message = "<h2>This is a test email</h2><p>Testing, Testing, Testing</p>";
EmailThread emailThread1 = new EmailThread(email, subject, message);
emailThread1.start();
try {
emailThread1.join();
} catch (InterruptedException e) {
}
String errorMessage1 = emailThread1.getErrorMessage();
if (errorMessage1.isEmpty()) {
System.out.println("Message sent to " + toEmailAddress);
} else {
System.out.println("Message not sent to " + toEmailAddress + " - " + errorMessage1);
}
}
EmailThread emailThread1 = new EmailThread(email, subject, message);
emailThread1.start();
try {
emailThread1.join();
} catch (InterruptedException e) {
}
String errorMessage1 = emailThread1.getErrorMessage();
<p class="my-3 text-body-secondary">Don't have an account? <a href="${appURL}/signup">Sign-up</a>
<br><a href="${appURL}/reset-password">Reset your password</a></p>
Test the program on localhost. You should receive an email with the password reset link.
The password reset link will look like this:
http://localhost:8080/project_name_war_exploded/new-password?key=17d5e524-d14b-49f6-8be6-0835247da38b
Check the database by running this query:
SELECT * FROM password_reset;
Deploy the app to Azure and test again. The password reset link will include azurewebsites.net instead of localhost.
Clicking either link will take you to a page not found. We will create that next.
Make the uuid longer and harder to guess by encrypting it.
https://codepen.io/tutsplus/pen/aboBgLX?editors=1010
https://brand.uiowa.edu/html-email-templates
Begin class with 30 minutes of worktime
CREATE PROCEDURE sp_get_password_reset(
IN p_token VARCHAR(255)
)
BEGIN
SELECT id, email, created_at
FROM password_reset
WHERE token = p_token;
END;
CREATE PROCEDURE sp_delete_password_reset(
IN p_id int
)
BEGIN
DELETE FROM password_reset WHERE id = p_id;
END;
CREATE PROCEDURE sp_update_user_password(
IN p_email VARCHAR(255),
IN p_password VARCHAR(255)
)
BEGIN
UPDATE user
SET password = p_password
WHERE email = p_email;
END;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/new-password")
public class NewPassword extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String token = req.getParameter("token");
req.setAttribute("token", token);
req.setAttribute("pageTitle", "New password");
req.getRequestDispatcher("WEB-INF/new-password.jsp").forward(req, resp);
}
}
<main>
<section class="p-0 d-flex align-items-center position-relative overflow-hidden">
<div class="container-fluid">
<div class="row">
<div class="col-12 col-lg-8 m-auto">
<div class="row my-5">
<div class="col-sm-10 col-xl-8 m-auto">
<h2>New password</h2>
<p class="lead mb-4">Please enter your new password.</p>
<c:if test="${not empty newPasswordFail}">
<div class="alert alert-danger mb-2" role="alert">
${newPasswordFail}
</div>
</c:if>
<!-- Form START -->
<form method="post" action="${appURL}/new-password">
<!-- Password -->
<div class="mb-4">
<label for="password1" class="form-label">Password *</label>
<div class="input-group input-group-lg">
<span class="input-group-text bg-light rounded-start border-0 text-secondary px-3"><i class="fas fa-lock"></i></span>
<input type="password" class="form-control <c:if test="${not empty password1Error }">is-invalid</c:if> border-0 bg-light rounded-end ps-1" placeholder="*********" id="password1" name="password1" value="${password1}">
<c:if test="${not empty password1Error }"><div class="invalid-feedback">${password1Error}</div></c:if>
</div>
</div>
<!-- Confirm Password -->
<div class="mb-4">
<label for="password2" class="form-label">Confirm Password *</label>
<div class="input-group input-group-lg">
<span class="input-group-text bg-light rounded-start border-0 text-secondary px-3"><i class="fas fa-lock"></i></span>
<input type="password" class="form-control <c:if test="${not empty password2Error }">is-invalid</c:if> border-0 bg-light rounded-end ps-1" placeholder="*********" id="password2" name="password2" value="${password2}">
<c:if test="${not empty password2Error }"><div class="invalid-feedback">${password2Error}</div></c:if>
</div>
</div>
<!-- Hidden field -->
<input type="hidden" name="token" value="${token}">
<!-- Button -->
<div class="align-items-center mt-0">
<div class="d-grid">
<button class="btn btn-primary mb-0" type="submit">Submit</button>
</div>
</div>
</form>
<!-- Form END -->
</div>
</div>
</div>
</div>
</div>
</section>
</main>
public static String getPasswordReset(String token) {
String email = "";
try (Connection connection = getConnection();
CallableStatement statement = connection.prepareCall("{CALL sp_get_password_reset(?)}")) {
statement.setString(1, token);
ResultSet resultSet = statement.executeQuery();
if (resultSet.next()) {
Instant now = Instant.now();
Instant created_at = resultSet.getTimestamp("created_at").toInstant();
Duration duration = Duration.between(created_at, now);
long minutesElapsed = duration.toMinutes();
if(minutesElapsed < 30) {
email = resultSet.getString("email");
}
int id = resultSet.getInt("id");
CallableStatement statement2 = connection.prepareCall("{CALL sp_delete_password_reset(?)}");
statement2.setInt(1, id);
statement2.executeUpdate();
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
return email;
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String password1 = req.getParameter("password1");
String password2 = req.getParameter("password2");
String token = req.getParameter("token");
req.setAttribute("password1", password1);
req.setAttribute("password2", password2);
req.setAttribute("token", token);
User user = new User();
boolean errorFound = false;
try {
user.setPassword(password1.toCharArray());
} catch(IllegalArgumentException e) {
errorFound = true;
req.setAttribute("password1Error", e.getMessage());
}
if(password2 != null && password2.equals("")) {
errorFound = true;
req.setAttribute("password2Error", "Please confirm your password");
}
if(password1 != null && password2 != null && !password2.equals(password1)) {
errorFound = true;
req.setAttribute("password2Error", "Passwords don't match");
}
if(token == null || token.equals("")) {
req.setAttribute("newPasswordFail", "Invalid or missing token");
}
if(!errorFound) {
String email = UserDAO.getPasswordReset(token);
if(email == null || email.equals("")) {
req.setAttribute("newPasswordFail", "Token not found");
} else {
boolean passwordUpdated = UserDAO.updatePassword(email, password1);
if(passwordUpdated) {
// Send confirmation email
String subject = "New Password Created";
String message = "<h2>New Password Created</h2>";
message += "<p>Your password has changed. If you suspect that someone else changed your password, please reset it with this link:</p>";
String appURL = "";
if (req.isSecure()) {
appURL = req.getServletContext().getInitParameter("appURLCloud");
} else {
appURL = req.getServletContext().getInitParameter("appURLLocal");
}
String fullURL = String.format("%s/reset-password", appURL);
message += String.format("<p><a href=\"%s\" target=\"_blank\">%s</a></p>", fullURL, fullURL);
// send email
EmailThread emailThread = new EmailThread(email, subject, message);
emailThread.start();
try {
emailThread.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// Redirect the user to the login page
HttpSession session = req.getSession(); // get an existing session if one exists
session.setAttribute("flashMessageSuccess", "New password has been created. Please sign in.");
resp.sendRedirect(resp.encodeRedirectURL(req.getContextPath() + "/login")); // Redirects the user to the login page
return;
} else {
req.setAttribute("newPasswordFail", "Could not reset your password.");
}
}
}
req.setAttribute("pageTitle", "New password");
req.getRequestDispatcher("WEB-INF/new-password.jsp").forward(req, resp);
}
public static boolean updatePassword(String email, String password) {
try (Connection connection = getConnection()) {
if (connection != null) {
try (CallableStatement statement = connection.prepareCall("{CALL sp_update_user_password(?, ?)}")) {
statement.setString(1, email);
String encryptedPassword = BCrypt.hashpw(password, BCrypt.gensalt(12));
statement.setString(2, encryptedPassword);
int rowsAffected = statement.executeUpdate();
return rowsAffected == 1;
}
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
return false;
}
By Marc Hauschildt
Web Technologies and Computer Software Development Instructor at Kirkwood Community College in Cedar Rapids, IA.