Skip to content

Commit 8c3bbee

Browse files
authored
Merge pull request #37 from BUMETCS673/stacey_dev_2.0
Implement User Authentication
2 parents 2f6cff0 + dccd10b commit 8c3bbee

20 files changed

+799
-82
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,17 @@ server/ # Java backend (to be added)
9494
This project uses job data via the **Rise Jobs API**: [https://pitchwall.co/product/rise-jobs-api](https://pitchwall.co/product/rise-jobs-api).
9595
Please review and respect the provider’s terms of service and attribution guidelines.
9696

97+
## Security
98+
### JWT Secret Setup
99+
- **Development:** No setup required!
100+
The app auto-generates a secure secret on first run and saves it to `~/.jwt-secret`.
101+
102+
- **Production:** You must provide a real secret via
103+
- Environment variable: `APP_JWT_SECRET`, or
104+
- Spring property: `app.jwt.secret`
105+
106+
If no secret is provided in production, the app will fail to start.
107+
97108
## License
98109

99110
For educational use as part of BU CS673 course project.

code/backend/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,6 @@ build/
3131

3232
### VS Code ###
3333
.vscode/
34+
35+
### Security ###
36+
~/.jwt-secret

code/backend/pom.xml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,30 @@
6060
<artifactId>h2</artifactId>
6161
<scope>runtime</scope>
6262
</dependency>
63+
6364
<!-- Use the modern MySQL driver artifact; let Boot manage the version -->
6465
<dependency>
6566
<groupId>com.mysql</groupId>
6667
<artifactId>mysql-connector-j</artifactId>
6768
<scope>runtime</scope>
6869
</dependency>
6970

71+
<dependency>
72+
<groupId>io.jsonwebtoken</groupId>
73+
<artifactId>jjwt-api</artifactId>
74+
<version>0.11.5</version>
75+
</dependency>
76+
<dependency>
77+
<groupId>io.jsonwebtoken</groupId>
78+
<artifactId>jjwt-impl</artifactId>
79+
<version>0.11.5</version>
80+
<scope>runtime</scope>
81+
</dependency>
82+
<dependency>
83+
<groupId>io.jsonwebtoken</groupId>
84+
<artifactId>jjwt-jackson</artifactId>
85+
<version>0.11.5</version>
86+
</dependency>
7087
<!-- Flyway migrations -->
7188
<dependency>
7289
<groupId>org.flywaydb</groupId>
@@ -104,6 +121,11 @@
104121
<artifactId>mysql</artifactId>
105122
<scope>test</scope>
106123
</dependency>
124+
<dependency>
125+
<groupId>com.h2database</groupId>
126+
<artifactId>h2</artifactId>
127+
<scope>test</scope>
128+
</dependency>
107129
</dependencies>
108130

109131
<build>

code/backend/src/main/java/com/cs673/careerforge/configs/SecurityBeansConfig.java

Lines changed: 0 additions & 35 deletions
This file was deleted.

code/backend/src/main/java/com/cs673/careerforge/configs/SecurityConfig.java

Lines changed: 75 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,31 +7,90 @@
77
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
88
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
99
import org.springframework.security.web.SecurityFilterChain;
10+
import com.cs673.careerforge.security.JwtRequestFilter;
11+
import com.cs673.careerforge.security.JwtUtil;
12+
import org.springframework.beans.factory.annotation.Autowired;
13+
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
14+
import org.springframework.security.config.http.SessionCreationPolicy;
15+
import org.springframework.security.core.userdetails.User;
16+
import org.springframework.security.core.userdetails.UserDetails;
17+
import org.springframework.security.core.userdetails.UserDetailsService;
18+
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
19+
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
20+
import org.springframework.security.crypto.password.PasswordEncoder;
21+
import org.springframework.beans.factory.annotation.Value;
22+
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
23+
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
24+
import com.cs673.careerforge.security.JwtAuthenticationEntryPoint;
1025

1126
@Configuration
1227
public class SecurityConfig {
1328

1429
@Bean
15-
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
16-
http
17-
.csrf(csrf -> csrf.disable())
18-
// allow H2 console to render in a frame
19-
.headers(h -> h.frameOptions(f -> f.sameOrigin()))
20-
.authorizeHttpRequests(auth -> auth
21-
.requestMatchers(
22-
"/actuator/health",
23-
"/h2-console/**",
24-
"/public/**"
25-
).permitAll()
26-
.anyRequest().authenticated()
27-
)
28-
.httpBasic(Customizer.withDefaults());
29-
30-
return http.build();
30+
public SecurityFilterChain filterChain(HttpSecurity http,
31+
JwtRequestFilter jwtRequestFilter,
32+
JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint) throws Exception {
33+
//csrf.disable -> typical for stateless APIs
34+
return http
35+
.csrf(csrf -> csrf.disable())
36+
// allow H2 console to render in a frame
37+
.headers(h -> h.frameOptions(f -> f.sameOrigin()))
38+
.authorizeHttpRequests(auth -> auth
39+
.requestMatchers(
40+
//For testing uncomment out the first line below to allow all endpoints to be unauthenticated
41+
//"/",
42+
"/actuator/health",
43+
"/h2-console/**",
44+
"/public/**",
45+
"/authenticate", // change for login endpoint
46+
"/register" // change for signup endpoint
47+
).permitAll()
48+
.requestMatchers("/secure").authenticated() // secure endpoint requires JWT
49+
// .requestMatchers("/secure").hasRole("USER") // secure endpoint requires JWT, both USER and ADMIN get in
50+
.requestMatchers("/admin/**").hasRole("ADMIN") //only ADMIN. Not implemented yet. Just being used for tests
51+
.anyRequest().authenticated()
52+
)
53+
.sessionManagement(session -> session
54+
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // no HTTP sessions
55+
)
56+
// plug in the custom entrypoint
57+
.exceptionHandling(ex ->
58+
ex.authenticationEntryPoint(jwtAuthenticationEntryPoint)
59+
)
60+
.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class)
61+
.build();
3162
}
3263

3364
@Bean
3465
public AuthenticationManager authenticationManager(AuthenticationConfiguration cfg) throws Exception {
3566
return cfg.getAuthenticationManager();
3667
}
68+
69+
@Bean
70+
public UserDetailsService users(
71+
@Value("${app.security.user.name}") String userName,
72+
@Value("${app.security.user.password}") String userPass,
73+
@Value("${app.security.admin.name}") String adminName,
74+
@Value("${app.security.admin.password}") String adminPass) {
75+
76+
PasswordEncoder encoder = passwordEncoder();
77+
78+
UserDetails user = User.withUsername(userName)
79+
.password(encoder.encode(userPass))
80+
.roles("USER")
81+
.build();
82+
83+
UserDetails admin = User.withUsername(adminName)
84+
.password(encoder.encode(adminPass))
85+
.roles("ADMIN")
86+
.build();
87+
88+
return new InMemoryUserDetailsManager(user, admin);
89+
}
90+
91+
@Bean
92+
public PasswordEncoder passwordEncoder() {
93+
return new BCryptPasswordEncoder();
94+
}
3795
}
96+
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.cs673.careerforge.controllers;
2+
3+
import org.springframework.web.bind.annotation.GetMapping;
4+
import org.springframework.web.bind.annotation.RequestMapping;
5+
import org.springframework.web.bind.annotation.RestController;
6+
7+
@RestController
8+
@RequestMapping("/admin")
9+
public class AdminController {
10+
11+
@GetMapping("/dashboard")
12+
public String dashboard() {
13+
return "Admin dashboard works!";
14+
}
15+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.cs673.careerforge.model;
2+
3+
import com.cs673.careerforge.model.AuthRequest;
4+
import com.cs673.careerforge.model.AuthResponse;
5+
import com.cs673.careerforge.security.JwtUtil;
6+
import org.springframework.beans.factory.annotation.Autowired;
7+
import org.springframework.security.authentication.AuthenticationManager;
8+
import org.springframework.security.authentication.BadCredentialsException;
9+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
10+
import org.springframework.security.core.userdetails.UserDetails;
11+
import org.springframework.security.core.userdetails.UserDetailsService;
12+
import org.springframework.web.bind.annotation.*;
13+
14+
@RestController
15+
public class AuthController {
16+
17+
@Autowired
18+
private AuthenticationManager authenticationManager;
19+
20+
@Autowired
21+
private JwtUtil jwtUtil;
22+
23+
@Autowired
24+
private UserDetailsService userDetailsService;
25+
26+
@PostMapping("/authenticate")
27+
public AuthResponse createAuthToken(@RequestBody AuthRequest authRequest) throws Exception {
28+
try {
29+
authenticationManager.authenticate(
30+
new UsernamePasswordAuthenticationToken(authRequest.getUsername(), authRequest.getPassword())
31+
);
32+
//For debugging below
33+
//System.out.println(">>> /authenticate called with: " + authRequest.getUsername());
34+
} catch (BadCredentialsException e) {
35+
throw new Exception("Incorrect username or password", e);
36+
}
37+
38+
final UserDetails userDetails = userDetailsService.loadUserByUsername(authRequest.getUsername());
39+
final String jwt = jwtUtil.generateToken(userDetails);
40+
41+
return new AuthResponse(jwt);
42+
}
43+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.cs673.careerforge.controllers;
2+
3+
import org.springframework.web.bind.annotation.GetMapping;
4+
import org.springframework.web.bind.annotation.RestController;
5+
import org.springframework.security.core.Authentication;
6+
7+
8+
@RestController
9+
public class SecureController {
10+
11+
@GetMapping("/secure")
12+
public String secureEndpoint(Authentication authentication) {
13+
boolean isAdmin = authentication.getAuthorities().stream()
14+
.anyMatch(auth -> auth.getAuthority().equals("ROLE_ADMIN"));
15+
16+
if (isAdmin) {
17+
return "Hello Admin: " + authentication.getName();
18+
} else {
19+
return "Hello User: " + authentication.getName();
20+
}
21+
}
22+
23+
@GetMapping("/public/hello")
24+
public String publicHello() {
25+
return "👋 Anyone can see this!";
26+
}
27+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import org.springframework.boot.actuate.health.Health;
2+
import org.springframework.boot.actuate.health.HealthIndicator;
3+
import org.springframework.stereotype.Component;
4+
import com.cs673.careerforge.security.JwtUtil;
5+
6+
@Component
7+
public class JwtHealthIndicator implements HealthIndicator {
8+
9+
private final JwtUtil jwtUtil;
10+
11+
public JwtHealthIndicator(JwtUtil jwtUtil) {
12+
this.jwtUtil = jwtUtil;
13+
}
14+
15+
@Override
16+
public Health health() {
17+
try {
18+
if (jwtUtil.getSigningKey() != null) {
19+
return Health.up()
20+
.withDetail("jwt", "Secret key is loaded")
21+
.build();
22+
} else {
23+
return Health.down()
24+
.withDetail("jwt", "Secret key is null")
25+
.build();
26+
}
27+
} catch (Exception e) {
28+
return Health.down(e).build();
29+
}
30+
}
31+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.cs673.careerforge.model;
2+
3+
//For testing to hold login credentials from the request
4+
5+
public class AuthRequest {
6+
private String username;
7+
private String password;
8+
9+
public AuthRequest(String username, String password) {
10+
this.username = username;
11+
this.password = password;
12+
}
13+
14+
public String getUsername() {
15+
return username;
16+
}
17+
18+
public String getPassword() {
19+
return password;
20+
}
21+
22+
public void setUsername(String username) {
23+
this.username = username;
24+
}
25+
26+
public void setPassword(String password) {
27+
this.password = password;
28+
}
29+
30+
}

0 commit comments

Comments
 (0)