This is an example Java web project with simple session based authentication for 420-N34-LA Java Web Programming Course - Champlain College
CREATE database SimpleSessionAuthentication;
CREATE USER 'simpleauthenticationuser'@'localhost' IDENTIFIED WITH mysql_native_password
BY 'simpleauthenticationuser';
GRANT ALL PRIVILEGES ON * . * TO 'simpleauthenticationuser'@'localhost';
CREATE TABLE SimpleSessionAuthentication.users (
`id` int NOT NULL AUTO_INCREMENT,
`email` varchar(45) NOT NULL UNIQUE,
`password` varchar(200) NOT NULL,
`fullname` varchar(45) NOT NULL,
PRIMARY KEY (`id`)
);
YOU SHOULD NEVER SAVE RAW PASSWORDS IN YOUR DATABASE!
What we can do is encrypt the password in a one way encryption method. That way if the database gets stolen or hacked, it will be difficult to find the real passwords of users.
A popular encryption algorithm used is BCrypt. One advantage of BCrypt over other algorithms is that it is slow! Not slow enough to cause a problem when you are using it in your app. But slow enough for someone to try and encrypt many passwords!
If a database gets hacked with encrypted passwords, one way to try and find what is the actual password of an encrypted password is to have a very big dictionary of possible passwords, encrypt all of them and check if any of them matches the encrypted password we have in the database.
Having a slow encryption algorithm can make this more difficult to succeed!
The algorithm has a cost parameter which specifies "how much work" is needed for the encryption to be done. In this application I am using cost=12. As the cost increases the time to do the encryption increases exponentially!
When a user is signing up, the first step is to verify that the password and the repeated password are equal.
Then the password is hashed using BCrypt hashing and the information are stored in the database.
We don't need to explicitly check if the email already exist because the email column is UNIQUE
in the table. So an error will occur. However, right now the app does not tell the user that the email already exist. So that's an improvement you can do.
Once the signup is successful, the application will dispatch to the signin page with a message saying creating an account was successful.
When the user signs in, we get the user information from the database using the email. And then we verify that the password is the same as the password stored. Of course the password that they write is not hashed (encrypted), so in our case we use the library's verifier to verify that the password results in the same hashed password.
If this is successful, we store in the session
the user object. And we dispatch to the home page.
AuthHelper is a class that has some static functions that helps us with the authentication.
For BCrypt I am using bcrypt open source library. It is a standalone library.
This method takes the raw password and returns the hashed password using bcrypt
library as a String.
public static String hashPassword(String pass) {
return BCrypt.withDefaults().hashToString(12, pass.toCharArray());
}
This method takes the raw password and the hashed password and returns true if the password is the hashed password are verified.
The verifyer is part of the bcrypt
library.
public static boolean verifyPassword(String pass, String hashedPass) {
BCrypt.Result result = BCrypt.verifyer().verify(pass.toCharArray(), hashedPass);
return result.verified;// result.verified;
}
Whenever we have a user logged in successfully, we will store the user object in the session. These two methods are just used to get and set the user object from the session. If the user does not exist then the value returned will be null.
public static User isLoggedIn(HttpSession session) {
return (User) session.getAttribute("user");
}
public static void saveUserInSession(HttpSession session, User user) {
session.setAttribute("user", user);
}
In the META-INF/context.xml
file we define the db connection resource
<Resource name="jdbc/simpleauthapp"
auth="Container" type="javax.sql.DataSource"
maxTotal="20" maxIdle="5" maxWaitMillis="10000"
username="simpleauthenticationuser" password="simpleauthenticationuser"
driverClassName="com.mysql.cj.jdbc.Driver"
url="jdbc:mysql://localhost:3306/simplesessionauthentication?useSSL=false&serverTimezone=UTC"/>
This is using the user we created in the SQL to connect. We granted the user full privileges (not secure but easy for local development).
The database helper class has two methods.
public boolean insertNewUser(User user, String hashedPass)
We pass the information of the user with a User object and the hashed password. The new user will be stored in the database (assuming everything is good).
public User getUser(String email, String password)
This method takes an email and password. Gets the user from the db and compares the password. If the user is found and the password is verified then it returns a User object with the user information. Otherwise it returns null.
Note: in this case getting the user from the database and verifying the password are both happening in this method. It might be a better design to separate the two functionalities depending on your app. The reason I did it this way, to not have the hashed password stored in the database leave this method. Notice that the User class does not have a member to store a password even.
The project is using MVC structure. The Servlets act as the controller. All requests come to the servlets. The servlet can:
- Access the model classes
DbHelper
,AuthHelper
, andUser
. - Perform some logic based on data it retrieved from the model, for example check if there isn't a logged in user and redirect to the sign in page.
- Put data in the request that was retrieved from the model, for example the logged in user object.
- And it can dispatch to the view, which in our case are the
jsp
files.
There are 4 Servlets.
-
Home
which has a url pattern of/index.html
. So if we access the web app at the root URL it will access this servlet. This servlet will check if we have a user stored in the session then it will put the user object in the request and dispatch to thehome.jsp
page. If there isn't a user in the session (user not logged in), it will dispatch to thesignin.jsp
page. -
Logout
will invalidate the session.session.invalidate();
meaning, any objects bounded to the session will be cleared. This will clear any user object that we have stored before in the session. After clearing the user from the session, it redirects to the index of the site. Notice there's a difference between dispatching and redirect. Dispatching happens on the server before the response returns to the browser. In redirecting, the redirect happens on the browser side. Which is why we send the redirect on the response object:response.sendRedirect("./");
-
Signin
:- If the user is already signed in, redirect the user to the home page.
- If the signin form was submitted and the user is authenticated successfully, put the user object in the request and dispatch to
home.jsp
else put an error message in the request and dispatch tosignin.jsp
page. - If the signin form was not submitted and the user is not logged in already, just dispatch to the
signin.jsp
page.
-
Signup
:- If the user is already signed in, redirect the user to the home page.
- If the signup form was submitted insert a new user to the db. If the new user is inserted successfylly, dispatch to
signin.jsp
with a message in the request telling the user that their account was created and they can sign in now. If inserting the user caused an error put an error message in the request and dispatch tosignup.jsp
page. - If the signup form was not submitted and the user is not logged in already, just dispatch to the
signup.jsp
page.
I am using mdboostrap which is material design built using bootstrap 5. I have used multiple page templates from the online documentation.
The forms for sign in and sign up uses POST method. This is important because the data that the request will have from the form includes the password. You don't want your password to be visible in the url! The action is the servlet name.
In each form there's a hidden input which I am using to check if the form was submitted. However another way I could have done this is by processing the form in the doPost
method of the servlet. Right now the doPost
method basically calls the doGet
. If the form is processed in the doPost
it means we don't have to check if a hidden input exist, because the request will be a post only when we submit the form.
To display a message on the page, I added a simple toast that is displayed if a message
value exist.
<c:if test="${not empty message}">
<div class="alert fixed-top ${messageClass}" role="alert">
<strong> ${message} </strong>
</div>
</c:if>
From the servlet, before dispatching to the jsp, you can set the message
and messageClass
values:
request.setAttribute("message", "Error signing in!");
request.setAttribute("messageClass", "alert-danger");
The message class is css class from bootstrap. I am using alert-danger
for errors and alert-success
for successful messages.
In the WEB-INF/lib
folder, I have 5 jar files.
For the bcrypt
library
bcrypt-0.9.0.jar
bytes-1.5.0.jar
JSTL library (c:if
)
jakarta.servlet.jsp.jstl-1.2.6.jar
jakarta.servlet.jsp.jstl-api-1.2.7.jar
MySQL connector
mysql-connector-java-8.0.13.jar