Skip to content

Commit 9644c82

Browse files
committed
feat: Add comprehensive security framework with JWT authentication, SecurityConfig, and role-based access control
1 parent 5006805 commit 9644c82

File tree

9 files changed

+1299
-0
lines changed

9 files changed

+1299
-0
lines changed
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
package jazzyframework.security;
2+
3+
import jazzyframework.di.DIContainer;
4+
import jazzyframework.routing.Router;
5+
import jazzyframework.security.annotations.EnableJazzyAuth;
6+
import jazzyframework.security.controller.DefaultAuthController;
7+
import jazzyframework.security.jwt.JwtUtil;
8+
import jazzyframework.security.validation.UserEntityValidator;
9+
import jazzyframework.security.config.SecurityConfig;
10+
import jazzyframework.data.BaseRepository;
11+
12+
import java.util.Set;
13+
import java.util.logging.Logger;
14+
15+
/**
16+
* Processes @EnableJazzyAuth annotation and automatically registers authentication endpoints.
17+
*
18+
* <p>This processor is responsible for:
19+
* <ul>
20+
* <li>Scanning for @EnableJazzyAuth annotations on application classes</li>
21+
* <li>Validating user entity classes for required authentication fields</li>
22+
* <li>Creating and configuring JWT utilities</li>
23+
* <li>Registering authentication endpoints (register, login, me)</li>
24+
* <li>Setting up SecurityInterceptor when SecurityConfig is available</li>
25+
* </ul>
26+
*
27+
* <p>The processor automatically integrates with the DI container to:
28+
* <ul>
29+
* <li>Obtain user repository instances</li>
30+
* <li>Register authentication controllers</li>
31+
* <li>Configure security interceptors</li>
32+
* </ul>
33+
*
34+
* @since 0.5.0
35+
* @author Caner Mastan
36+
*/
37+
public class AuthProcessor {
38+
39+
private static final Logger logger = Logger.getLogger(AuthProcessor.class.getName());
40+
private final Router router;
41+
private final DIContainer diContainer;
42+
private JwtUtil jwtUtil; // Store for SecurityInterceptor
43+
44+
/**
45+
* Creates a new AuthProcessor with the specified router and DI container.
46+
*
47+
* @param router The router to register authentication endpoints
48+
* @param diContainer The DI container for dependency management
49+
*/
50+
public AuthProcessor(Router router, DIContainer diContainer) {
51+
this.router = router;
52+
this.diContainer = diContainer;
53+
}
54+
55+
/**
56+
* Scans for @EnableJazzyAuth annotation and configures authentication.
57+
*
58+
* <p>This method performs the following steps:
59+
* <ol>
60+
* <li>Scans all registered classes in the DI container</li>
61+
* <li>Looks for classes annotated with @EnableJazzyAuth</li>
62+
* <li>Configures authentication based on the annotation parameters</li>
63+
* <li>Falls back to classpath scanning if needed</li>
64+
* </ol>
65+
*/
66+
public void processAuthAnnotations() {
67+
try {
68+
// Get all registered classes from DI container
69+
Set<Class<?>> allClasses = diContainer.getAllRegisteredClasses();
70+
71+
for (Class<?> clazz : allClasses) {
72+
if (clazz.isAnnotationPresent(EnableJazzyAuth.class)) {
73+
EnableJazzyAuth authConfig = clazz.getAnnotation(EnableJazzyAuth.class);
74+
configureAuthentication(authConfig);
75+
logger.info("Authentication configured from @EnableJazzyAuth on " + clazz.getSimpleName());
76+
return; // Only process first found annotation
77+
}
78+
}
79+
80+
// Also scan all classes in classpath for @EnableJazzyAuth (fallback approach)
81+
scanClasspathForAuthAnnotation();
82+
83+
} catch (Exception e) {
84+
logger.warning("Failed to process @EnableJazzyAuth annotations: " + e.getMessage());
85+
e.printStackTrace();
86+
}
87+
}
88+
89+
/**
90+
* Scans classpath for @EnableJazzyAuth annotation using stack trace analysis.
91+
* This is a fallback method when the annotation is not found in DI container.
92+
*/
93+
private void scanClasspathForAuthAnnotation() {
94+
try {
95+
// Get stack trace to find the main class
96+
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
97+
98+
for (StackTraceElement element : stackTrace) {
99+
if ("main".equals(element.getMethodName())) {
100+
try {
101+
Class<?> mainClass = Class.forName(element.getClassName());
102+
if (mainClass.isAnnotationPresent(EnableJazzyAuth.class)) {
103+
EnableJazzyAuth authConfig = mainClass.getAnnotation(EnableJazzyAuth.class);
104+
configureAuthentication(authConfig);
105+
logger.info("Authentication configured from @EnableJazzyAuth on " + mainClass.getSimpleName());
106+
return;
107+
}
108+
} catch (ClassNotFoundException e) {
109+
// Continue scanning
110+
}
111+
}
112+
}
113+
114+
logger.fine("No @EnableJazzyAuth annotation found");
115+
} catch (Exception e) {
116+
logger.warning("Failed to scan classpath for @EnableJazzyAuth: " + e.getMessage());
117+
}
118+
}
119+
120+
/**
121+
* Configures authentication based on @EnableJazzyAuth annotation parameters.
122+
*
123+
* <p>This method:
124+
* <ul>
125+
* <li>Validates the user entity class</li>
126+
* <li>Creates JWT utility with specified configuration</li>
127+
* <li>Obtains user repository from DI container</li>
128+
* <li>Creates and registers authentication controller</li>
129+
* <li>Registers authentication endpoints</li>
130+
* <li>Sets up security interceptor if SecurityConfig exists</li>
131+
* </ul>
132+
*
133+
* @param config The @EnableJazzyAuth annotation configuration
134+
* @throws RuntimeException if authentication configuration fails
135+
*/
136+
private void configureAuthentication(EnableJazzyAuth config) {
137+
try {
138+
// Validate user class
139+
UserEntityValidator.validateUserClass(config.userClass(), config.loginMethod());
140+
141+
// Create JWT utility
142+
jwtUtil = new JwtUtil(config.jwtSecret(), config.jwtExpirationHours());
143+
144+
// Get user repository from DI container
145+
BaseRepository<Object, Long> userRepository = getUserRepository(config.repositoryClass());
146+
147+
// Create auth controller
148+
DefaultAuthController authController = new DefaultAuthController(
149+
config.userClass(),
150+
config.loginMethod(),
151+
jwtUtil,
152+
userRepository
153+
);
154+
155+
// Register auth controller in DI container
156+
diContainer.registerInstance(DefaultAuthController.class, authController);
157+
158+
// Register auth endpoints
159+
String basePath = config.authBasePath();
160+
router.POST(basePath + "/register", "register", DefaultAuthController.class);
161+
router.POST(basePath + "/login", "login", DefaultAuthController.class);
162+
router.GET(basePath + "/me", "getCurrentUser", DefaultAuthController.class);
163+
164+
logger.info("Authentication endpoints registered:");
165+
logger.info(" POST " + basePath + "/register");
166+
logger.info(" POST " + basePath + "/login");
167+
logger.info(" GET " + basePath + "/me");
168+
logger.info("Using repository: " + config.repositoryClass().getSimpleName());
169+
170+
// Set up SecurityInterceptor if SecurityConfig exists
171+
setupSecurityInterceptor();
172+
173+
} catch (Exception e) {
174+
throw new RuntimeException("Failed to configure authentication", e);
175+
}
176+
}
177+
178+
/**
179+
* Sets up SecurityInterceptor if a SecurityConfig implementation is found in the DI container.
180+
*
181+
* <p>This method scans for classes extending SecurityConfig and creates a SecurityInterceptor
182+
* instance to handle URL-based security rules.
183+
*/
184+
private void setupSecurityInterceptor() {
185+
try {
186+
// Look for SecurityConfig in DI container
187+
Set<Class<?>> allClasses = diContainer.getAllRegisteredClasses();
188+
189+
for (Class<?> clazz : allClasses) {
190+
if (SecurityConfig.class.isAssignableFrom(clazz) && !SecurityConfig.class.equals(clazz)) {
191+
SecurityConfig securityConfig = (SecurityConfig) diContainer.getBean(clazz);
192+
SecurityInterceptor interceptor = new SecurityInterceptor(securityConfig, jwtUtil);
193+
194+
// Register interceptor in DI container for Router to use
195+
diContainer.registerInstance(SecurityInterceptor.class, interceptor);
196+
197+
logger.info("SecurityInterceptor configured with: " + clazz.getSimpleName());
198+
return;
199+
}
200+
}
201+
202+
logger.fine("No SecurityConfig found, authentication endpoints only");
203+
} catch (Exception e) {
204+
logger.warning("Failed to setup SecurityInterceptor: " + e.getMessage());
205+
}
206+
}
207+
208+
/**
209+
* Gets the user repository from the DI container.
210+
*
211+
* @param repositoryClass The repository class to obtain
212+
* @return The repository instance cast to BaseRepository
213+
* @throws RuntimeException if the repository cannot be found or is invalid
214+
*/
215+
@SuppressWarnings("unchecked")
216+
private BaseRepository<Object, Long> getUserRepository(Class<?> repositoryClass) {
217+
try {
218+
// Get the repository from DI container
219+
Object repository = diContainer.getBean(repositoryClass);
220+
if (repository instanceof BaseRepository) {
221+
return (BaseRepository<Object, Long>) repository;
222+
} else {
223+
throw new IllegalArgumentException("Repository class must extend BaseRepository: " + repositoryClass.getSimpleName());
224+
}
225+
} catch (Exception e) {
226+
throw new RuntimeException("Failed to get user repository: " + repositoryClass.getSimpleName() +
227+
". Make sure it's registered in DI container.", e);
228+
}
229+
}
230+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package jazzyframework.security;
2+
3+
import jazzyframework.http.Request;
4+
import jazzyframework.http.Response;
5+
import jazzyframework.http.JSON;
6+
import jazzyframework.security.config.SecurityConfig;
7+
import jazzyframework.security.jwt.JwtUtil;
8+
import jazzyframework.security.jwt.JwtClaims;
9+
10+
import java.util.logging.Logger;
11+
12+
/**
13+
* Security interceptor that validates JWT tokens based on SecurityConfig rules.
14+
*
15+
* <p>This interceptor automatically checks incoming requests against the configured
16+
* security rules and validates JWT tokens for protected endpoints.
17+
*
18+
* <p>The interceptor works by:
19+
* <ul>
20+
* <li>Checking if the requested endpoint is explicitly marked as public</li>
21+
* <li>Validating JWT tokens for secure endpoints</li>
22+
* <li>Verifying user roles for admin-only endpoints</li>
23+
* <li>Returning appropriate HTTP status codes (401, 403) for security violations</li>
24+
* </ul>
25+
*
26+
* @since 0.5.0
27+
* @author Caner Mastan
28+
*/
29+
public class SecurityInterceptor {
30+
31+
private static final Logger logger = Logger.getLogger(SecurityInterceptor.class.getName());
32+
private final SecurityConfig securityConfig;
33+
private final JwtUtil jwtUtil;
34+
35+
/**
36+
* Creates a new SecurityInterceptor with the specified configuration.
37+
*
38+
* @param securityConfig The security configuration defining endpoint rules
39+
* @param jwtUtil The JWT utility for token validation
40+
*/
41+
public SecurityInterceptor(SecurityConfig securityConfig, JwtUtil jwtUtil) {
42+
this.securityConfig = securityConfig;
43+
this.jwtUtil = jwtUtil;
44+
45+
// Initialize security config
46+
this.securityConfig.configure();
47+
48+
logger.info("Security interceptor initialized");
49+
logger.info("Public endpoints: " + securityConfig.getPublicEndpoints());
50+
logger.info("Secure endpoints: " + securityConfig.getSecureEndpoints());
51+
logger.info("Admin endpoints: " + securityConfig.getAdminEndpoints());
52+
}
53+
54+
/**
55+
* Intercepts an incoming request and applies security rules.
56+
*
57+
* @param request The HTTP request to check
58+
* @return null if the request should proceed, or a Response if it should be blocked
59+
*/
60+
public Response intercept(Request request) {
61+
String path = request.getPath();
62+
String method = request.getMethod();
63+
64+
logger.fine("Security check for: " + method + " " + path);
65+
66+
// Check if endpoint is explicitly public
67+
if (securityConfig.isPublic(path)) {
68+
logger.fine("Public endpoint, allowing access");
69+
return null; // Allow request to proceed
70+
}
71+
72+
// Check if endpoint requires authentication
73+
if (securityConfig.requiresAuth(path)) {
74+
logger.fine("Secure endpoint, checking JWT token");
75+
76+
// Validate JWT token
77+
JwtClaims claims = validateToken(request);
78+
if (claims == null) {
79+
logger.info("Authentication failed for: " + method + " " + path);
80+
return Response.json(JSON.of("error", "Authentication required")).status(401);
81+
}
82+
83+
// Check admin role if required
84+
if (securityConfig.requiresAdmin(path)) {
85+
if (!claims.hasRole("ADMIN")) {
86+
logger.info("Admin access denied for user " + claims.getUserId() + " on: " + path);
87+
return Response.json(JSON.of("error", "Admin access required")).status(403);
88+
}
89+
}
90+
91+
logger.fine("Authentication successful for user: " + claims.getUserId());
92+
}
93+
94+
return null; // Allow request to proceed
95+
}
96+
97+
/**
98+
* Validates JWT token from the Authorization header.
99+
*
100+
* @param request The HTTP request containing the Authorization header
101+
* @return JwtClaims if token is valid, null otherwise
102+
*/
103+
private JwtClaims validateToken(Request request) {
104+
try {
105+
String authHeader = request.header("Authorization");
106+
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
107+
return null;
108+
}
109+
110+
String token = authHeader.substring(7);
111+
return jwtUtil.validateToken(token);
112+
113+
} catch (Exception e) {
114+
logger.fine("JWT validation failed: " + e.getMessage());
115+
return null;
116+
}
117+
}
118+
}

0 commit comments

Comments
 (0)