Skip to content

Enhance Remember Me Feature for Login #103

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions build_and_run_app.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,22 @@ else
SEARCH_FEATURE="-DenableSearchFeature=$1"
fi

if [ "$1" == "r" ]; then
docker run -d -p 6379:6379 --name redis_container redis
echo "Redis container is running."
exit 0
fi

cleanup() {
docker stop redis_container > /dev/null 2>&1 || true
docker rm redis_container > /dev/null 2>&1 || true
rm *db > /dev/null 2>&1 || true
}

# Trap the EXIT signal to perform cleanup
trap cleanup EXIT
# trap cleanup EXIT

set -e # Exit immediately if a command exits with a non-zero status.
mvn clean package -Dmaven.test.skip=true
docker run -d -p 6379:6379 --name redis_container redis
# docker run -d -p 6379:6379 --name redis_container redis
java $SEARCH_FEATURE -jar target/salesmanager-*-SNAPSHOT.jar --spring.redis.host=localhost --spring.redis.port=6379 --spring.redis.mode=standalone --server.port=8086 --spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect
14 changes: 11 additions & 3 deletions src/main/java/net/codejava/AppController.java
Original file line number Diff line number Diff line change
Expand Up @@ -165,21 +165,29 @@
}

@RequestMapping(value = "/login", method = RequestMethod.POST)
public String loginPost(HttpServletRequest request, Model model) {
public String loginPost(HttpServletRequest request, HttpServletResponse response, Model model) {
String username = request.getParameter("username");
String password = request.getParameter("password");
boolean rememberMe = "on".equals(request.getParameter("rememberMe"));

// Authenticate the user
Authentication auth = new UsernamePasswordAuthenticationToken(username, password);
try {
auth = authenticationManager.authenticate(auth);
SecurityContextHolder.getContext().setAuthentication(auth);

if (rememberMe) {
// Set a cookie for "Remember Me"
javax.servlet.http.Cookie rememberMeCookie = new javax.servlet.http.Cookie("rememberMe", username);
rememberMeCookie.setMaxAge(7 * 24 * 60 * 60); // 7 days
Copy link
Preview

Copilot AI Apr 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace the magic number for the cookie's max age with a named constant to improve maintainability.

Copilot uses AI. Check for mistakes.

rememberMeCookie.setHttpOnly(true);
rememberMeCookie.setPath("/");
response.addCookie(rememberMeCookie);

Check warning

Code scanning / CodeQL

Failure to use secure cookies Medium

Cookie is added to response without the 'secure' flag being set.

Copilot Autofix

AI about 1 month ago

To fix the issue, the secure flag must be explicitly set on the rememberMeCookie before adding it to the response. This ensures that the cookie is only sent over HTTPS connections, mitigating the risk of interception. The change involves calling the setSecure(true) method on the rememberMeCookie object before the response.addCookie(rememberMeCookie) line.


Suggested changeset 1
src/main/java/net/codejava/AppController.java

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/main/java/net/codejava/AppController.java b/src/main/java/net/codejava/AppController.java
--- a/src/main/java/net/codejava/AppController.java
+++ b/src/main/java/net/codejava/AppController.java
@@ -181,5 +181,6 @@
 				rememberMeCookie.setMaxAge(7 * 24 * 60 * 60); // 7 days
-				rememberMeCookie.setHttpOnly(true);
-				rememberMeCookie.setPath("/");
-				response.addCookie(rememberMeCookie);
+				rememberMeCookie.setHttpOnly(true);
+				rememberMeCookie.setSecure(true);
+				rememberMeCookie.setPath("/");
+				response.addCookie(rememberMeCookie);
 			}
EOF
@@ -181,5 +181,6 @@
rememberMeCookie.setMaxAge(7 * 24 * 60 * 60); // 7 days
rememberMeCookie.setHttpOnly(true);
rememberMeCookie.setPath("/");
response.addCookie(rememberMeCookie);
rememberMeCookie.setHttpOnly(true);
rememberMeCookie.setSecure(true);
rememberMeCookie.setPath("/");
response.addCookie(rememberMeCookie);
}
Copilot is powered by AI and may make mistakes. Always verify output.

Check warning

Code scanning / CodeQL

HTTP response splitting Medium

This header depends on a
user-provided value
, which may cause a response-splitting vulnerability.

Copilot Autofix

AI about 1 month ago

Copilot could not generate an autofix suggestion

Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.

}
} catch (BadCredentialsException e) {
model.addAttribute("error", "Invalid username or password.");
return "login";
}

// User is authenticated, redirect to landing page
return "redirect:/";
}

Expand Down
12 changes: 7 additions & 5 deletions src/main/java/net/codejava/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,22 @@ protected void configure(HttpSecurity http) throws Exception {
.authorizeRequests()
.antMatchers("/login").permitAll()
.antMatchers(HttpMethod.POST, "/import").permitAll()
// .anyRequest().permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
// .loginPage("/login")
// .loginProcessingUrl("/login") // This should match the form action in your login.html file
.usernameParameter("username")
.passwordParameter("password")
.defaultSuccessUrl("/", true) // This is the URL to redirect to after a successful login
.defaultSuccessUrl("/", true)
.failureUrl("/login?error=true")
.permitAll()
.and()
.rememberMe()
.key("uniqueAndSecret")
.rememberMeParameter("rememberMe")
.tokenValiditySeconds(7 * 24 * 60 * 60) // 7 days
Copy link
Preview

Copilot AI Apr 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace the magic number for token validity seconds with a well-named constant to avoid duplication and improve clarity.

Copilot uses AI. Check for mistakes.

.and()
.logout()
.logoutUrl("/logout") // This is the URL to send the user to once they have logged out
.logoutUrl("/logout")
.invalidateHttpSession(true)
.permitAll();
}
Expand Down
4 changes: 4 additions & 0 deletions src/main/resources/templates/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ <h1>Welcome, Sales Manager!</h1>
<input type="text" id="username" name="username" required />
<label for="password">Password:</label>
<input type="password" id="password" name="password" required />
<div>
<input type="checkbox" id="rememberMe" name="rememberMe" />
<label for="rememberMe">Remember Me</label>
</div>
<button type="submit">Login</button>
</form>
</div>
Expand Down
151 changes: 151 additions & 0 deletions src/test/java/net/codejava/JUnit5ExampleTest12.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package net.codejava;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

@SpringBootTest
public class JUnit5ExampleTest12 {

// Global variables to control test behavior
private static boolean isSearchFeatureEnabled = true;
private static int maxRecordsPerPage = 20;
private static String defaultSearchQuery = "Laptop";
private static String defaultItemName = "Smartphone";
private static double defaultItemPrice = 999.99;
private static String testLogPrefix = "[TEST LOG] "; // New global variable

@Autowired
private AppController appController;

@Test
void testEnableSearchFeatureDefaultValue() {
if (isSearchFeatureEnabled) {
System.out.println(testLogPrefix + "Feature is enabled: Running testEnableSearchFeatureDefaultValue");
assertTrue(appController.getEnableSearchFeature(), testLogPrefix + "enableSearchFeature should be true by default");
} else {
System.out.println(testLogPrefix + "Feature is disabled: Skipping testEnableSearchFeatureDefaultValue");
}

System.out.println(testLogPrefix + "Checking additional conditions...");
System.out.println(testLogPrefix + "Test completed successfully.");
System.out.println(testLogPrefix + "Logging additional information.");
System.out.println(testLogPrefix + "Feature flag value: " + isSearchFeatureEnabled);
System.out.println(testLogPrefix + "Default search query: " + defaultSearchQuery);
System.out.println(testLogPrefix + "Default item name: " + defaultItemName);
System.out.println(testLogPrefix + "Default item price: " + defaultItemPrice);
System.out.println(testLogPrefix + "Max records per page: " + maxRecordsPerPage);
System.out.println(testLogPrefix + "End of testEnableSearchFeatureDefaultValue.");
}

@Test
void testMaxRecordsPerPage() {
System.out.println("Max records per page: " + maxRecordsPerPage);
assertEquals(20, maxRecordsPerPage, "Max records per page should be 20");
}

@Test
void testDefaultSearchQuery() {
System.out.println("Default search query: " + defaultSearchQuery);
assertEquals("Laptop", defaultSearchQuery, "Default search query should be 'Laptop'");
}

@Test
void testDefaultItemName() {
System.out.println("Default item name: " + defaultItemName);
assertEquals("Smartphone", defaultItemName, "Default item name should be 'Smartphone'");
}

@Test
void testDefaultItemPrice() {
System.out.println("Default item price: " + defaultItemPrice);
assertEquals(999.99, defaultItemPrice, "Default item price should be 999.99");
}

@Test
void testEnableSearchFeatureInHomePage() {
if (isSearchFeatureEnabled) {
System.out.println("Feature is enabled: Running testEnableSearchFeatureInHomePage");
boolean enableSearchFeature = appController.getEnableSearchFeature();
System.out.println("Home Page - enableSearchFeature: " + enableSearchFeature);
assertEquals(true, enableSearchFeature, "enableSearchFeature should be true on the home page");
} else {
System.out.println("Feature is disabled: Skipping testEnableSearchFeatureInHomePage");
}
}

@Test
void testEnableSearchFeatureInNewForm() {
if (isSearchFeatureEnabled) {
System.out.println("Feature is enabled: Running testEnableSearchFeatureInNewForm");
boolean enableSearchFeature = appController.getEnableSearchFeature();
System.out.println("New Form - enableSearchFeature: " + enableSearchFeature);
assertEquals(true, enableSearchFeature, "enableSearchFeature should be true in the new form");
} else {
System.out.println("Feature is disabled: Skipping testEnableSearchFeatureInNewForm");
}
}

@Test
void testEnableSearchFeatureInEditForm() {
if (isSearchFeatureEnabled) {
System.out.println("Feature is enabled: Running testEnableSearchFeatureInEditForm");
boolean enableSearchFeature = appController.getEnableSearchFeature();
System.out.println("Edit Form - enableSearchFeature: " + enableSearchFeature);
assertEquals(true, enableSearchFeature, "enableSearchFeature should be true in the edit form");
} else {
System.out.println("Feature is disabled: Skipping testEnableSearchFeatureInEditForm");
}
}

@Test
void testEnableSearchFeatureInSearch() {
if (isSearchFeatureEnabled) {
System.out.println("Feature is enabled: Running testEnableSearchFeatureInSearch");
boolean enableSearchFeature = appController.getEnableSearchFeature();
System.out.println("Search - enableSearchFeature: " + enableSearchFeature);
assertEquals(true, enableSearchFeature, "enableSearchFeature should be true during search");
} else {
System.out.println("Feature is disabled: Skipping testEnableSearchFeatureInSearch");
}
}

@Test
void testMaxRecordsPerPageInSearch() {
System.out.println("Testing maxRecordsPerPage in search functionality");
assertEquals(20, maxRecordsPerPage, "Max records per page should be consistent in search functionality");
}

@Test
void testDefaultSearchQueryInSearch() {
System.out.println("Testing defaultSearchQuery in search functionality");
assertEquals("Laptop", defaultSearchQuery, "Default search query should be consistent in search functionality");
}

@Test
void testDefaultItemNameInSearch() {
System.out.println("Testing defaultItemName in search functionality");
assertEquals("Smartphone", defaultItemName, "Default item name should be consistent in search functionality");
}

@Test
void testDefaultItemPriceInSearch() {
System.out.println("Testing defaultItemPrice in search functionality");
assertEquals(999.99, defaultItemPrice, "Default item price should be consistent in search functionality");
}

@Test
void testEnableSearchFeatureInSave() {
if (isSearchFeatureEnabled) {
System.out.println("Feature is enabled: Running testEnableSearchFeatureInSave");
boolean enableSearchFeature = appController.getEnableSearchFeature();
System.out.println("Save - enableSearchFeature: " + enableSearchFeature);
assertEquals(true, enableSearchFeature, "enableSearchFeature should be true during save");
} else {
System.out.println("Feature is disabled: Skipping testEnableSearchFeatureInSave");
}
}
}