Skip to content
Open
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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -189,4 +189,6 @@ build/
!**/src/test/**/build/

### VS Code ###
.vscode/
.vscode/

.env
42 changes: 42 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,48 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.6</version>
</dependency>

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
<dependency>
<groupId>io.github.cdimascio</groupId>
<artifactId>java-dotenv</artifactId>
<version>5.2.2</version>
</dependency>


<dependency>
<groupId>org.postgresql</groupId>
Expand All @@ -63,6 +101,10 @@
<artifactId>spring-boot-starter-webmvc-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;


@SpringBootApplication
public class SecurityApplication {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,32 @@
import lombok.AllArgsConstructor;
import net.hackyourfuture.security.authentication.dto.LoginRequest;
import net.hackyourfuture.security.authentication.dto.LoginResponse;
import net.hackyourfuture.security.user.User;
import net.hackyourfuture.security.user.UserRepository;
import org.springframework.http.HttpStatus;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;

@Service
@AllArgsConstructor
public class AuthenticationService {

private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwtService jwtService;

public LoginResponse login(LoginRequest request) {
throw new UnsupportedOperationException("TODO: implement login");
User user = userRepository.findByUsername(request.username());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

User user = userRepository.findByUsername(request.username())
            .orElseThrow(() -> new ResponseStatusException(HttpStatus.UNAUTHORIZED

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

this is the same functionality, but maybe looks cleaner.


if (user == null || !passwordEncoder.matches(request.password(), user.getPassword())) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid username or password");
}

return new LoginResponse(jwtService.generateToken(user.getUsername()));
}

public void logout() {
throw new UnsupportedOperationException("TODO: implement logout");
// JWT logout: client deletes token
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package net.hackyourfuture.security.authentication;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import net.hackyourfuture.security.user.User;
import net.hackyourfuture.security.user.UserRepository;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.List;

@Component
@AllArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtService jwtService;
private final UserRepository userRepository;

@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {

String header = request.getHeader("Authorization");

if (header == null || !header.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}

String token = header.substring(7);

if (!jwtService.isTokenValid(token)) {
filterChain.doFilter(request, response);
return;
}

String username = jwtService.extractUsername(token);
User user = userRepository.findByUsername(username);

if (user != null) {
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
user.getUsername(),
null,
List.of()
);

SecurityContextHolder.getContext().setAuthentication(authentication);
}

filterChain.doFilter(request, response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package net.hackyourfuture.security.authentication;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;

@Service
public class JwtService {

@Value("${jwt.secret}")
private String secret;

private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}

public String generateToken(String username) {
return Jwts.builder()
.subject(username)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60))
.signWith(getSigningKey())
.compact();
}

public String extractUsername(String token) {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload()
.getSubject();
}

public boolean isTokenValid(String token) {
try {
extractUsername(token);
return true;
} catch (Exception e) {
return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,44 @@
package net.hackyourfuture.security.config;

import lombok.AllArgsConstructor;
import net.hackyourfuture.security.authentication.JwtAuthenticationFilter;
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.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@AllArgsConstructor
public class SecurityConfig {

private final JwtAuthenticationFilter jwtAuthenticationFilter;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll());
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/users/register", "/auth/login").permitAll()
.requestMatchers("/users/profile", "/auth/logout").authenticated()
.anyRequest().authenticated()
)
.addFilterBefore(
jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class
);

return http.build();
}
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.security.core.Authentication;

@RestController
@RequestMapping("/users")
Expand All @@ -25,7 +26,7 @@ public UserResponse register(@RequestBody UserRequest request) {
}

@GetMapping("/profile")
public UserResponse profile() {
return userService.getProfile("REPLACE WITH CURRENTLY LOGGED IN USER ID");
public UserResponse profile(Authentication authentication) {
return userService.getProfile(authentication.getName());
}
}
22 changes: 21 additions & 1 deletion src/main/java/net/hackyourfuture/security/user/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,44 @@
import net.hackyourfuture.security.user.dto.UserRequest;
import net.hackyourfuture.security.user.dto.UserResponse;
import org.springframework.http.HttpStatus;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;

import java.util.UUID;

@Service
@AllArgsConstructor
public class UserService {

private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;

public UserResponse register(UserRequest request) {
throw new UnsupportedOperationException("TODO: implement registration");
User existingUser = userRepository.findByUsername(request.username());

if (existingUser != null) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "Username already exists");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nice that you are giving informative messages!

}

User user = new User(
UUID.randomUUID().toString(),
request.username(),
passwordEncoder.encode(request.password())
);

userRepository.createUser(user);

return new UserResponse(user.getId(), user.getUsername());
}

public UserResponse getProfile(String username) {
User user = userRepository.findByUsername(username);

if (user == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "User not found");
}

return new UserResponse(user.getId(), user.getUsername());
}
}
11 changes: 8 additions & 3 deletions src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@ spring:
application:
name: security
datasource:
url: '${DB_URL}'
username: '${DB_USERNAME}'
password: '${DB_PASSWORD}'
url: ${DB_URL}
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
driver-class-name: org.postgresql.Driver
jwt:
secret: ${JWT_SECRET}
sql:
init:
mode: always