Duration: 12-16 weeks
Goal: Establish secure, modern foundation for all future development
Prerequisites: Development environment setup (see DEVELOPMENT.md)
This phase addresses critical security vulnerabilities and modernizes core infrastructure. All work in this phase can begin immediately and provides immediate value.
- Release Management & Versioning
- Authentication & Authorization System
- Dependency Updates & Security Patches
- Database Modernization
Priority: Start immediately (parallel with other work)
Duration: 2 weeks
Why First: Establishes process for tracking all subsequent changes
/doc/releaseNotes.txt - 65K of unstructured release notes
/doc/release.txt - 238K of release history
/doc/version.txt - Version tracking in text files
No GitHub Releases
No semantic versioning
No changelogs
Release process unclear
✅ Semantic Versioning (SemVer 2.0)
✅ GitHub Releases with changelogs
✅ CHANGELOG.md in repository
✅ Automated version tagging
✅ Release notes visible on website
✅ Link to roadmap from website
Action:
# Review existing version files
cat doc/version.txt
cat doc/release.txt | head -50
# Determine last significant release
# Current version appears to be 8.x based on database migrationsDecision:
- Current version:
8.x.x(based on db/upgrade/8.x structure) - Starting point for new versioning:
8.105.0or9.0.0 - Recommendation: Use
9.0.0to mark new development era
Create: .version file in repository root
9.0.0
Document: Create docs/VERSIONING.md
# AntWeb Versioning
AntWeb follows Semantic Versioning 2.0 (https://semver.org/)
## Version Format: MAJOR.MINOR.PATCH
- **MAJOR:** Breaking changes, major architecture updates
- **MINOR:** New features, non-breaking changes
- **PATCH:** Bug fixes, security patches
## Examples
- `9.0.0` → `9.1.0`: New authentication system (Phase 1)
- `9.1.0` → `9.1.1`: Security patch for authentication
- `9.1.1` → `10.0.0`: API breaking changes
## Release Schedule
- **Patch releases:** As needed (security/bugs)
- **Minor releases:** Monthly during active development
- **Major releases:** Per roadmap phases
## Version Locations
- Repository: `.version` file
- GitHub: Release tags (v9.0.0)
- Website: Footer and /about page
- API: /api/v3/version endpointCreate: CHANGELOG.md in repository root
# Changelog
All notable changes to AntWeb will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- Semantic versioning system
- GitHub Releases integration
- This CHANGELOG file
## [9.0.0] - 2026-01-XX
### Changed
- Established new release management process
- Migrated release notes from text files to GitHub
### Migration Note
This version marks the beginning of structured release management.
Previous releases were documented in /doc/release.txt and /doc/releaseNotes.txt.
## Historical Releases
See `/doc/release.txt` for releases prior to v9.0.0.Action:
- Create first GitHub Release:
# Tag current state as baseline
git tag -a v9.0.0 -m "Release 9.0.0: Establish release management"
git push origin v9.0.0-
On GitHub:
- Go to https://github.com/calacademy-research/antweb/releases
- Click "Draft a new release"
- Tag:
v9.0.0 - Title:
Release 9.0.0: New Release Management - Description: Copy from CHANGELOG.md
-
Create release template:
Create: .github/RELEASE_TEMPLATE.md
## What's Changed
### Added
-
### Changed
-
### Fixed
-
### Security
-
## Migration Notes
[Any breaking changes or upgrade steps]
## Contributors
Thanks to everyone who contributed to this release!
**Full Changelog**: https://github.com/calacademy-research/antweb/compare/v[PREVIOUS]...v[CURRENT]Purpose: Allow programmatic version checking
Create: api/v3/version.py
from flask import jsonify, current_app
import os
@app.route('/api/v3/version')
def get_version():
"""Return current AntWeb version."""
# Read from .version file
version_file = os.path.join(current_app.root_path, '../../.version')
try:
with open(version_file, 'r') as f:
version = f.read().strip()
except FileNotFoundError:
version = "unknown"
return jsonify({
'version': version,
'api_version': '3.1',
'release_notes': 'https://github.com/calacademy-research/antweb/releases',
'roadmap': 'https://github.com/calacademy-research/antweb/blob/master/docs/ROADMAP.md'
})Test:
curl http://localhost:5000/api/v3/versionExpected response:
{
"version": "9.0.0",
"api_version": "3.1",
"release_notes": "https://github.com/calacademy-research/antweb/releases",
"roadmap": "https://github.com/calacademy-research/antweb/blob/master/docs/ROADMAP.md"
}Location 1: Footer (all pages)
Edit: web/common/footer.jsp
<div class="footer">
<div class="footer-links">
<a href="/about.do">About</a> |
<a href="/contact.do">Contact</a> |
<a href="https://github.com/calacademy-research/antweb/releases" target="_blank">Release Notes</a> |
<a href="https://github.com/calacademy-research/antweb/blob/master/docs/ROADMAP.md" target="_blank">Development Roadmap</a>
</div>
<div class="footer-version">
Version <%= application.getAttribute("version") %>
</div>
<div class="footer-copyright">
© California Academy of Sciences
</div>
</div>Location 2: About Page
Edit: web/about.jsp
<h2>Current Version</h2>
<p>
AntWeb <%= application.getAttribute("version") %>
<br>
<a href="https://github.com/calacademy-research/antweb/releases" target="_blank">
View Release Notes
</a>
<br>
<a href="https://github.com/calacademy-research/antweb/blob/master/docs/ROADMAP.md" target="_blank">
View Development Roadmap
</a>
</p>Location 3: Admin/Curator Dashboard
Edit: web/curate.jsp
<div class="admin-info">
<h3>System Information</h3>
<ul>
<li>Version: <%= application.getAttribute("version") %></li>
<li><a href="https://github.com/calacademy-research/antweb/releases" target="_blank">Release Notes</a></li>
<li><a href="https://github.com/calacademy-research/antweb/blob/master/docs/ROADMAP.md" target="_blank">Roadmap</a></li>
<li><a href="https://github.com/calacademy-research/antweb/issues" target="_blank">Report Issue</a></li>
</ul>
</div>Edit: src/org/calacademy/antweb/util/AntwebStartupListener.java (create if doesn't exist)
package org.calacademy.antweb.util;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class AntwebStartupListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent event) {
// Load version from .version file
String version = loadVersion();
event.getServletContext().setAttribute("version", version);
}
@Override
public void contextDestroyed(ServletContextEvent event) {
// Cleanup if needed
}
private String loadVersion() {
String versionFile = System.getProperty("catalina.base") + "/.version";
try (BufferedReader reader = new BufferedReader(new FileReader(versionFile))) {
return reader.readLine().trim();
} catch (IOException e) {
return "unknown";
}
}
}Register listener in: WEB-INF/web.xml
<listener>
<listener-class>org.calacademy.antweb.util.AntwebStartupListener</listener-class>
</listener>Create: docs/RELEASING.md
# Release Process
## Pre-Release Checklist
- [ ] All tests passing
- [ ] CHANGELOG.md updated
- [ ] Version bumped in `.version`
- [ ] Database migrations documented
- [ ] Breaking changes documented
- [ ] Security issues addressed
## Creating a Release
### 1. Update Version
```bash
# For patch release: 9.0.0 → 9.0.1
# For minor release: 9.0.1 → 9.1.0
# For major release: 9.1.0 → 10.0.0
echo "9.1.0" > .versionAdd new section to CHANGELOG.md:
## [9.1.0] - 2026-02-15
### Added
- New authentication system
- Password reset functionality
### Changed
- Updated all dependencies
### Security
- Fixed authentication vulnerability CVE-XXXXgit add .version CHANGELOG.md
git commit -m "Release 9.1.0"
git tag -a v9.1.0 -m "Release 9.1.0: Authentication system"
git push origin master
git push origin v9.1.0- Go to https://github.com/calacademy-research/antweb/releases/new
- Choose tag:
v9.1.0 - Title:
Release 9.1.0: Authentication System - Copy content from CHANGELOG.md
- Attach any release artifacts (if applicable)
- Publish release
# Follow deployment process
# Update version display on website- Email to antweb-announce mailing list
- Post to social media (if applicable)
- Update documentation
For urgent security fixes:
- Create hotfix branch from latest release tag
- Fix issue
- Bump patch version
- Create release
- Deploy immediately
---
### Success Criteria for Release Management
**Week 2 Complete When:**
- [ ] `.version` file in repository
- [ ] CHANGELOG.md established
- [ ] First GitHub Release (v9.0.0) created
- [ ] Version displayed on website footer
- [ ] Links to GitHub releases and roadmap on website
- [ ] API endpoint returns version
- [ ] docs/VERSIONING.md documented
- [ ] docs/RELEASING.md documented
---
## 2. Authentication & Authorization System
**Priority:** Start Week 3 (after release management established)
**Duration:** 8-10 weeks
**Critical:** Highest security priority
### Current State Assessment
#### Week 3: Security Audit
**Task 2.1: Document Current Authentication**
**Create:** `docs/CURRENT_AUTH_ANALYSIS.md`
```markdown
# Current Authentication System Analysis
## Password Storage
- [ ] Method: (MD5/SHA1/bcrypt/plain text?)
- [ ] Salt used: (yes/no)
- [ ] Iterations: (if applicable)
## Session Management
- [ ] Session timeout: (minutes)
- [ ] Session token storage: (cookie/server)
- [ ] HTTPS enforcement: (yes/no)
## User Roles
- [ ] Admin
- [ ] Curator
- [ ] Contributor
- [ ] Public (no login)
## Vulnerabilities Identified
1. [List security issues found]
## Code Locations
- LoginDb.java - Database access
- LoginAction.java - Login handling
- login.jsp - Login form
- [Other files]
Audit Commands:
-- Check password storage
SELECT id, email, password, LENGTH(password) as pwd_len
FROM login LIMIT 5;
-- Check user distribution
SELECT role, COUNT(*) FROM login GROUP BY role;
-- Check recent activity
SELECT COUNT(*) FROM login
WHERE last_login > DATE_SUB(NOW(), INTERVAL 6 MONTH);
-- Check for inactive accounts
SELECT email, last_login FROM login
WHERE last_login < DATE_SUB(NOW(), INTERVAL 1 YEAR)
OR last_login IS NULL;Task 2.2: Security Vulnerability Scan
# Run OWASP dependency check
./gradlew dependencyCheckAnalyze
# Run SQL injection scanner
sqlmap -u "http://localhost/login.do" --forms
# Check for XSS vulnerabilities
xsser -u "http://localhost/login.do" --auto
# SSL/TLS check
sslyze --regular localhost:443Document findings in: docs/SECURITY_AUDIT_2026.md
Task 2.3: Choose Authentication Framework
Options Evaluation:
| Framework | Pros | Cons | Recommendation |
|---|---|---|---|
| Spring Security | Industry standard, comprehensive, well-documented | Complex setup, Java only | ✅ RECOMMENDED |
| Apache Shiro | Simpler than Spring Security | Less active development | |
| Custom JWT | Full control, lightweight | More work, security risks | ❌ Not recommended |
| Auth0/Okta | Managed, modern | External dependency, cost |
Decision: Use Spring Security for:
- Industry-standard security
- Active development and support
- Comprehensive features (JWT, OAuth2, LDAP)
- Large community
Task 2.4: Design Database Schema
Create new tables:
-- New authentication tables
CREATE TABLE auth_user (
id INT PRIMARY KEY AUTO_INCREMENT,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL, -- bcrypt hash
full_name VARCHAR(255),
is_active BOOLEAN DEFAULT TRUE,
is_locked BOOLEAN DEFAULT FALSE,
failed_login_attempts INT DEFAULT 0,
last_login TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_email (email),
INDEX idx_active (is_active)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE auth_role (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) UNIQUE NOT NULL, -- ADMIN, CURATOR, CONTRIBUTOR
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE auth_user_role (
user_id INT NOT NULL,
role_id INT NOT NULL,
granted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
granted_by INT, -- admin who granted this role
PRIMARY KEY (user_id, role_id),
FOREIGN KEY (user_id) REFERENCES auth_user(id) ON DELETE CASCADE,
FOREIGN KEY (role_id) REFERENCES auth_role(id) ON DELETE CASCADE,
FOREIGN KEY (granted_by) REFERENCES auth_user(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE auth_permission (
id INT PRIMARY KEY AUTO_INCREMENT,
role_id INT NOT NULL,
resource VARCHAR(100) NOT NULL, -- e.g., "specimens", "images", "users"
action VARCHAR(50) NOT NULL, -- e.g., "create", "read", "update", "delete"
FOREIGN KEY (role_id) REFERENCES auth_role(id) ON DELETE CASCADE,
UNIQUE KEY unique_permission (role_id, resource, action)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE auth_audit_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id INT,
action VARCHAR(100) NOT NULL, -- LOGIN, LOGOUT, PASSWORD_CHANGE, etc.
ip_address VARCHAR(45),
user_agent TEXT,
success BOOLEAN NOT NULL,
failure_reason VARCHAR(255),
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES auth_user(id) ON DELETE SET NULL,
INDEX idx_user_id (user_id),
INDEX idx_timestamp (timestamp),
INDEX idx_action (action)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE auth_password_reset (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
token VARCHAR(64) UNIQUE NOT NULL,
expires_at TIMESTAMP NOT NULL,
used BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES auth_user(id) ON DELETE CASCADE,
INDEX idx_token (token),
INDEX idx_expires (expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE auth_session (
id VARCHAR(64) PRIMARY KEY,
user_id INT NOT NULL,
ip_address VARCHAR(45),
user_agent TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_activity TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
expires_at TIMESTAMP NOT NULL,
FOREIGN KEY (user_id) REFERENCES auth_user(id) ON DELETE CASCADE,
INDEX idx_user_id (user_id),
INDEX idx_expires (expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;Migration Strategy:
-- Migration script: Populate new auth tables from old login table
INSERT INTO auth_user (email, full_name, is_active, last_login, created_at)
SELECT
email,
name,
is_active,
last_login,
created
FROM login;
-- Map old roles to new roles
INSERT INTO auth_role (name, description) VALUES
('ADMIN', 'Full system access'),
('CURATOR', 'Can upload and edit specimens'),
('CONTRIBUTOR', 'Can submit specimens for review'),
('USER', 'Read-only access');
-- Assign roles based on old access_group
INSERT INTO auth_user_role (user_id, role_id)
SELECT
au.id,
ar.id
FROM auth_user au
JOIN login l ON au.email = l.email
JOIN auth_role ar ON (
CASE
WHEN l.role = 'admin' THEN ar.name = 'ADMIN'
WHEN l.role = 'curator' THEN ar.name = 'CURATOR'
ELSE ar.name = 'USER'
END
);Task 2.5: Set Up Spring Security
Add to: build.xml (or create pom.xml if moving to Maven)
<!-- Spring Security dependencies -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>6.2.1</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>6.2.1</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
<version>6.2.1</version>
</dependency>
<!-- JWT support -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>Task 2.6: Implement Core Authentication
Create: src/org/calacademy/antweb/security/SecurityConfig.java
package org.calacademy.antweb.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // 12 rounds
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/**").permitAll() // API has own auth
.requestMatchers("/public/**", "/browse.do", "/description.do").permitAll()
.requestMatchers("/upload.do", "/curate.do").hasAnyRole("CURATOR", "ADMIN")
.requestMatchers("/admin.do", "/util.do").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login.do")
.loginProcessingUrl("/performLogin")
.defaultSuccessUrl("/curate.do")
.failureUrl("/login.do?error=true")
.permitAll()
)
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
)
.sessionManagement(session -> session
.maximumSessions(1)
.expiredUrl("/login.do?expired=true")
)
.csrf(csrf -> csrf
.ignoringRequestMatchers("/api/**") // API uses JWT
);
return http.build();
}
}Create: src/org/calacademy/antweb/security/UserDetailsServiceImpl.java
package org.calacademy.antweb.security;
import org.springframework.security.core.userdetails.*;
import org.springframework.stereotype.Service;
import org.calacademy.antweb.home.AuthUserDb;
import java.sql.SQLException;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
try (Connection conn = DBUtil.getConnection()) {
AuthUserDb userDb = new AuthUserDb(conn);
AuthUser user = userDb.getUserByEmail(email);
if (user == null || !user.isActive()) {
throw new UsernameNotFoundException("User not found or inactive: " + email);
}
return User.builder()
.username(user.getEmail())
.password(user.getPasswordHash())
.roles(user.getRoles().toArray(new String[0]))
.accountLocked(user.isLocked())
.build();
} catch (SQLException e) {
throw new UsernameNotFoundException("Database error", e);
}
}
}Task 2.7: Implement Password Reset
Create: src/org/calacademy/antweb/security/PasswordResetService.java
package org.calacademy.antweb.security;
import java.security.SecureRandom;
import java.util.Base64;
import java.time.LocalDateTime;
public class PasswordResetService {
private static final int TOKEN_LENGTH = 32;
private static final int EXPIRY_HOURS = 24;
public String createResetToken(int userId) throws SQLException {
// Generate secure random token
SecureRandom random = new SecureRandom();
byte[] tokenBytes = new byte[TOKEN_LENGTH];
random.nextBytes(tokenBytes);
String token = Base64.getUrlEncoder().withoutPadding().encodeToString(tokenBytes);
// Store in database
try (Connection conn = DBUtil.getConnection()) {
String sql = "INSERT INTO auth_password_reset (user_id, token, expires_at) " +
"VALUES (?, ?, DATE_ADD(NOW(), INTERVAL ? HOUR))";
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setInt(1, userId);
stmt.setString(2, token);
stmt.setInt(3, EXPIRY_HOURS);
stmt.executeUpdate();
}
}
return token;
}
public boolean validateResetToken(String token) throws SQLException {
try (Connection conn = DBUtil.getConnection()) {
String sql = "SELECT id, expires_at, used FROM auth_password_reset " +
"WHERE token = ?";
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, token);
ResultSet rs = stmt.executeQuery();
if (!rs.next()) {
return false; // Token not found
}
if (rs.getBoolean("used")) {
return false; // Already used
}
Timestamp expires = rs.getTimestamp("expires_at");
if (expires.before(new Timestamp(System.currentTimeMillis()))) {
return false; // Expired
}
return true;
}
}
}
public void resetPassword(String token, String newPassword) throws SQLException {
if (!validateResetToken(token)) {
throw new IllegalArgumentException("Invalid or expired token");
}
// Hash new password
PasswordEncoder encoder = new BCryptPasswordEncoder(12);
String hashedPassword = encoder.encode(newPassword);
try (Connection conn = DBUtil.getConnection()) {
// Get user ID from token
String getUserSql = "SELECT user_id FROM auth_password_reset WHERE token = ?";
int userId;
try (PreparedStatement stmt = conn.prepareStatement(getUserSql)) {
stmt.setString(1, token);
ResultSet rs = stmt.executeQuery();
if (!rs.next()) {
throw new SQLException("Token not found");
}
userId = rs.getInt("user_id");
}
// Update password
String updateSql = "UPDATE auth_user SET password_hash = ? WHERE id = ?";
try (PreparedStatement stmt = conn.prepareStatement(updateSql)) {
stmt.setString(1, hashedPassword);
stmt.setInt(2, userId);
stmt.executeUpdate();
}
// Mark token as used
String markUsedSql = "UPDATE auth_password_reset SET used = TRUE WHERE token = ?";
try (PreparedStatement stmt = conn.prepareStatement(markUsedSql)) {
stmt.setString(1, token);
stmt.executeUpdate();
}
// Log audit
AuditLogService.log(userId, "PASSWORD_RESET", true, null);
}
}
}Task 2.8: User Migration Script
Create: scripts/migrate-users.sh
#!/bin/bash
# Migrate users from old login table to new auth system
set -e
echo "Starting user migration..."
# Backup current login table
mysql -uantweb -p ant -e "CREATE TABLE login_backup AS SELECT * FROM login;"
# Create new auth tables
mysql -uantweb -p ant < db/migrations/auth-schema.sql
# Migrate users
mysql -uantweb -p ant < db/migrations/migrate-users.sql
# Verify migration
COUNT_OLD=$(mysql -uantweb -p ant -sN -e "SELECT COUNT(*) FROM login;")
COUNT_NEW=$(mysql -uantweb -p ant -sN -e "SELECT COUNT(*) FROM auth_user;")
if [ "$COUNT_OLD" -eq "$COUNT_NEW" ]; then
echo "✅ Migration successful: $COUNT_NEW users migrated"
else
echo "❌ Migration failed: $COUNT_OLD old users, $COUNT_NEW new users"
exit 1
fi
echo "Sending password reset emails to all users..."
# Run Java program to send reset emails
java -cp ... org.calacademy.antweb.security.PasswordResetEmailer
echo "Migration complete!"Task 2.9: Testing
Create: test/org/calacademy/antweb/security/AuthenticationTest.java
package org.calacademy.antweb.security;
import org.junit.Test;
import static org.junit.Assert.*;
public class AuthenticationTest {
@Test
public void testPasswordHashing() {
PasswordEncoder encoder = new BCryptPasswordEncoder(12);
String password = "TestPassword123!";
String hash = encoder.encode(password);
assertTrue(encoder.matches(password, hash));
assertFalse(encoder.matches("WrongPassword", hash));
}
@Test
public void testPasswordResetToken() throws SQLException {
PasswordResetService service = new PasswordResetService();
int userId = 1;
String token = service.createResetToken(userId);
assertNotNull(token);
assertTrue(service.validateResetToken(token));
// Token should be invalid after use
service.resetPassword(token, "NewPassword123!");
assertFalse(service.validateResetToken(token));
}
@Test
public void testAccountLocking() throws SQLException {
// Test that account locks after 5 failed attempts
// Test that account unlocks after timeout
// Test admin can manually unlock
}
}Manual Testing Checklist:
## Authentication Testing
- [ ] Login with valid credentials succeeds
- [ ] Login with invalid credentials fails
- [ ] Account locks after 5 failed attempts
- [ ] Password reset email sent
- [ ] Password reset link works
- [ ] Old password no longer works after reset
- [ ] Session expires after 30 minutes
- [ ] Logout clears session
- [ ] Cannot access admin pages without admin role
- [ ] Cannot access curator pages without curator/admin role
- [ ] Audit log records all login attempts
- [ ] XSS injection prevented
- [ ] SQL injection prevented
- [ ] CSRF protection worksWeek 10 Complete When:
- All tests passing (unit + integration)
- All users migrated successfully
- Password reset emails sent
- Security audit shows zero critical vulnerabilities
- Performance: Login takes < 1 second
- Audit logging functional
- Documentation complete
- Deployed to staging and tested
- Ready for production deployment
Priority: Week 11-13 (parallel with auth deployment)
Duration: 3 weeks
[Continue with detailed dependency update plan...]
Priority: Week 14-20
Duration: 6-7 weeks
[Continue with detailed database migration plan...]
- Semantic versioning established
- GitHub releases configured
- Website links added
- Release process documented
- Spring Security integrated
- All users migrated
- Password reset functional
- Audit logging operational
- Zero critical vulnerabilities
- All dependencies < 2 years old
- Automated scanning enabled
- MySQL 8 in production
- All tables using InnoDB
- Performance improved
- Zero data loss
Phase 1 is complete when:
-
Release Management:
- ✅ v9.x.x version in production
- ✅ GitHub releases with changelogs
- ✅ Links on website functional
-
Security:
- ✅ Zero critical vulnerabilities
- ✅ Modern authentication (bcrypt)
- ✅ Comprehensive audit logging
-
Dependencies:
- ✅ All packages current
- ✅ Automated scanning in CI/CD
-
Database:
- ✅ MySQL 8.0+ operational
- ✅ UTF-8MB4 throughout
- ✅ Measurable performance gains
-
Documentation:
- ✅ All changes documented
- ✅ Migration guides complete
- ✅ Testing procedures documented
Once Phase 1 is complete, Phase 2 begins:
- Complete REST API (see docs/ROADMAP.md)
- Background job system
- API documentation
- Issues: https://github.com/calacademy-research/antweb/issues
- Discussions: Use
phase-1label - Security: Email security@antweb.org (do not post publicly)
Last Updated: January 2026
Next Review: After Milestone 2 (Week 10)