Java 3 - 2025

Week 5

Password Reset Table

  • Query your database to create a password_reset table.
  • Create a stored procedure to insert records into this table.
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;

Reset Password Servlet

  • Create a new servlet called "ResetPassword". Add this code.
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);
    }
}

reset-password.jsp

  • Create a new JSP called "reset-password.jsp". Add this code.
<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>

Reset Password Servlet

  • Create a doPost method. Add this code.
  • We pass the request object to the passwordReset method so our program can determine if the request is coming from Azure or localhost.
@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>
  • Open login.jsp. Add a link to the reset password servlet.

UserDAO

  • If a user is found with the email entered, create a new database record, and send an email. The URL of the password reset hyperlink will be based on the host (Azure or localhost).
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";
}

Azure Email

  • In the shared package, add the AzureEmail class from Java 2 to send emails.
  • Create system environment variables (Windows | Mac) for AZURE_EMAIL_CONNECTION,  AZURE_EMAIL_FROM, and ADMIN_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 "";
    }
}

Helpers Class

  • In the shared package, add the Helpers class from Java 2.
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();
    }
}

Email Thread

  • In the shared package, add the EmailThread class from Java 2.
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;
    }
}

Send Test Email

  • Add a main method to the AzureEmail class to verify that it works.
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();
  • If successful, remove the main method and add these lines to the UserDAO.passwordReset method.

login.jsp

  • Add a link to /password-reset
<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.

High quality Emails

  • https://codepen.io/tutsplus/pen/aboBgLX?editors=1010

  • https://brand.uiowa.edu/html-email-templates

End Day 9

Start Day 10

Begin class with 30 minutes of worktime

Stored Procedures

  • Create stored procedures to get password reset tokens, delete password reset tokens, and update a user's password.
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;

New Password Servlet

  • Create a new servlet called "NewPassword". Add this code.
  • Be sure to include the parameter "key" in the doGet method.
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);
    }
}

new-password.jsp

  • Create a new JSP called "new-password.jsp". Add this code.
<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>

UserDAO

  • Write a getPasswordReset method that takes the token string as input and returns the email string.
  • If the token is not found or if the token was created over 30 minutes ago, return an empty string.
  • If the token is found and was created less than 30 minutes ago, return the email.
  • If the token is found, delete the token when finished.
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;
}

New Password Servlet

  • Create the doPost method to handle the form submission.
  • Note that the value returned from the getPasswordReset function will be an empty string if the token is not valid.
@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);
}

UserDAO

  • Add an updatePassword method to update the user's password.
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;
}
  • Run the program in Tomcat. Click the email link you previously received.
  • Assuming it has been over 30 minutes since you received the email, the code will not be valid.
  • Request a new password reset and test its functionality.
  • Deploy the app to Azure and test it there too.

End Day 10

Java 3 - Week 5

By Marc Hauschildt

Java 3 - Week 5

  • 234