diff --git a/skill-tree /src/main/java/com/RDS/skilltree/Authentication/AuthEntryPoint.java b/skill-tree /src/main/java/com/RDS/skilltree/Authentication/AuthEntryPoint.java new file mode 100644 index 00000000..37fb5170 --- /dev/null +++ b/skill-tree /src/main/java/com/RDS/skilltree/Authentication/AuthEntryPoint.java @@ -0,0 +1,31 @@ +package com.RDS.skilltree.Authentication; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.io.PrintWriter; + +@Component +public class AuthEntryPoint implements AuthenticationEntryPoint { + + /** + * A description of the entire Java function. + * + * @param request the HttpServletRequest object representing the client's request + * @param response the HttpServletResponse object representing the server's response + * @param authException the AuthenticationException object representing the exception that occurred during authentication + * @throws IOException if an I/O error occurs while writing the response + * @throws ServletException if the request could not be handled + */ + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + PrintWriter writer = response.getWriter(); + writer.println("Access Denied !! " + authException.getMessage()); + } +} diff --git a/skill-tree /src/main/java/com/RDS/skilltree/Authentication/UserAuthenticationToken.java b/skill-tree /src/main/java/com/RDS/skilltree/Authentication/UserAuthenticationToken.java new file mode 100644 index 00000000..b5464d78 --- /dev/null +++ b/skill-tree /src/main/java/com/RDS/skilltree/Authentication/UserAuthenticationToken.java @@ -0,0 +1,31 @@ +package com.RDS.skilltree.Authentication; + +import com.RDS.skilltree.User.UserModel; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import javax.security.auth.Subject; + +public class UserAuthenticationToken extends AbstractAuthenticationToken { + + private final UserModel user; + + public UserAuthenticationToken(UserModel user) { + super(null); + this.user = user; + setAuthenticated(true); + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public Object getPrincipal() { + return user; + } + + @Override + public boolean implies(Subject subject) { + return super.implies(subject); + } +} diff --git a/skill-tree /src/main/java/com/RDS/skilltree/Config/SecurityConfig.java b/skill-tree /src/main/java/com/RDS/skilltree/Config/SecurityConfig.java new file mode 100644 index 00000000..5550027f --- /dev/null +++ b/skill-tree /src/main/java/com/RDS/skilltree/Config/SecurityConfig.java @@ -0,0 +1,42 @@ +package com.RDS.skilltree.Config; + +import com.RDS.skilltree.Authentication.AuthEntryPoint; +import com.RDS.skilltree.Filters.JWTAuthenticationFilter; +import com.RDS.skilltree.User.UserService; +import org.springframework.beans.factory.annotation.Autowired; +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.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@EnableWebSecurity +@Configuration +public class SecurityConfig { + private final UserService userService; + private final AuthEntryPoint authEntryPoint; + + @Autowired + public SecurityConfig(UserService userService, AuthEntryPoint authEntryPoint) { + this.userService = userService; + this.authEntryPoint = authEntryPoint; + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http.csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(auth->auth.requestMatchers("/**").authenticated()) + .exceptionHandling(ex->ex.authenticationEntryPoint(authEntryPoint)) + .sessionManagement(session->session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); + return http.build(); + } + + @Bean + public JWTAuthenticationFilter jwtAuthenticationFilter(){ + return new JWTAuthenticationFilter(userService); + } +} diff --git a/skill-tree /src/main/java/com/RDS/skilltree/Filters/JWTAuthenticationFilter.java b/skill-tree /src/main/java/com/RDS/skilltree/Filters/JWTAuthenticationFilter.java new file mode 100644 index 00000000..3c2914ea --- /dev/null +++ b/skill-tree /src/main/java/com/RDS/skilltree/Filters/JWTAuthenticationFilter.java @@ -0,0 +1,87 @@ +package com.RDS.skilltree.Filters; + +import com.RDS.skilltree.Authentication.UserAuthenticationToken; +import com.RDS.skilltree.User.*; +import com.RDS.skilltree.utils.FetchAPI; +import com.RDS.skilltree.utils.JWTUtils; +import com.RDS.skilltree.utils.RDSUser.Response; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.net.URL; +import java.util.concurrent.CompletableFuture; + +@Slf4j +public class JWTAuthenticationFilter extends OncePerRequestFilter { + + @Autowired + private JWTUtils jwtUtils; + + @Autowired + private FetchAPI fetchAPI; + + private final UserService userService; + + @Autowired + private UserRepository userRepository; + + public JWTAuthenticationFilter(UserService userService){ + this.userService = userService; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + String token = getJWTFromRequest(request); + try { + if (StringUtils.hasText(token) && jwtUtils.validateToken(token)) { + String rdsUserId = jwtUtils.getRDSUserId(token); + + + UserModel userModel; + + if (userRepository.existsByRdsUserId(rdsUserId)) { + userModel = userRepository.findByRdsUserId(rdsUserId).get(); + } else { + CompletableFuture userResponse = fetchAPI.getRDSUserData(rdsUserId); + CompletableFuture.anyOf(userResponse).join(); + Response rdsUserResponse = userResponse.get(); + UserDRO userDRO = UserDRO.builder() + .rdsUserId(rdsUserId) + .firstName(rdsUserResponse.getUser().getFirst_name()) + .lastName(rdsUserResponse.getUser().getLast_name()) + .imageUrl(new URL(rdsUserResponse.getUser().getPicture().getUrl())) + .role(UserRole.USER) + .build(); + userModel = UserDRO.toModel(userDRO); + userService.createUser(userDRO); + } + + UserAuthenticationToken authentication = new UserAuthenticationToken(userModel); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } catch (Exception e) { + log.error("Error in fetching the user details, error : {}", e.getMessage(), e); + throw new RuntimeException(e); + } + filterChain.doFilter(request, response); + } + + private String getJWTFromRequest(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + return null; + } +} diff --git a/skill-tree /src/test/java/com/RDS/skilltree/User/UserServiceImplTest.java b/skill-tree /src/test/java/com/RDS/skilltree/User/UserServiceImplTest.java new file mode 100644 index 00000000..e9a28df2 --- /dev/null +++ b/skill-tree /src/test/java/com/RDS/skilltree/User/UserServiceImplTest.java @@ -0,0 +1,56 @@ +package com.RDS.skilltree.User; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.net.MalformedURLException; +import java.net.URL; + +import static org.junit.jupiter.api.Assertions.*; + + +@SpringBootTest +class UserServiceImplTest { + + private final UserService userService; + + @Autowired + public UserServiceImplTest(UserService userService) { + this.userService = userService; + } + + @BeforeEach + void setUp() { + + } + + @AfterEach + void tearDown() { + + } + + @Test + public void testCreateUser() throws MalformedURLException { + UserDRO user1 = new UserDRO("12345abcd","John", "Doe",new URL("https://example.com"), UserType.MEMBER,UserRole.USER); + userService.createUser(user1); + } + + @Test + void updateUser() { + } + + @Test + void getUserById() { + } + + @Test + void getAllUsers() { + } + + @Test + void addSkill() { + } +} \ No newline at end of file diff --git a/skill-tree/.gitignore b/skill-tree/.gitignore index 549e00a2..66853d59 100644 --- a/skill-tree/.gitignore +++ b/skill-tree/.gitignore @@ -31,3 +31,4 @@ build/ ### VS Code ### .vscode/ +dev.env \ No newline at end of file diff --git a/skill-tree/pom.xml b/skill-tree/pom.xml index 76cb6a90..11e8c325 100644 --- a/skill-tree/pom.xml +++ b/skill-tree/pom.xml @@ -15,6 +15,7 @@ skill tree project 17 + 0.11.2 1.17.6 @@ -85,6 +86,31 @@ junit-jupiter test + + org.springframework.boot + spring-boot-starter-security + + + io.jsonwebtoken + jjwt-api + 0.11.2 + + + io.jsonwebtoken + jjwt-api + ${io.jsonwebtoken.version} + + + io.jsonwebtoken + jjwt-impl + ${io.jsonwebtoken.version} + runtime + + + io.jsonwebtoken + jjwt-jackson + ${io.jsonwebtoken.version} + diff --git a/skill-tree/src/main/java/com/RDS/skilltree/SkillTreeApplication.java b/skill-tree/src/main/java/com/RDS/skilltree/SkillTreeApplication.java index 1393b0f6..d0e7f01b 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/SkillTreeApplication.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/SkillTreeApplication.java @@ -3,10 +3,11 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; + @SpringBootApplication public class SkillTreeApplication { - public static void main(String[] args) { - SpringApplication.run(SkillTreeApplication.class, args); - } + public static void main(String[] args) { + SpringApplication.run(SkillTreeApplication.class, args); + } } diff --git a/skill-tree/src/main/java/com/RDS/skilltree/User/UserRepository.java b/skill-tree/src/main/java/com/RDS/skilltree/User/UserRepository.java index 9bba0027..518931b9 100644 --- a/skill-tree/src/main/java/com/RDS/skilltree/User/UserRepository.java +++ b/skill-tree/src/main/java/com/RDS/skilltree/User/UserRepository.java @@ -3,8 +3,12 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.Optional; import java.util.UUID; @Repository public interface UserRepository extends JpaRepository { + Optional findByRdsUserId(String rdsUserId); + + Boolean existsByRdsUserId(String rdsUserId); } diff --git a/skill-tree/src/main/java/com/RDS/skilltree/utils/FetchAPI.java b/skill-tree/src/main/java/com/RDS/skilltree/utils/FetchAPI.java new file mode 100644 index 00000000..503d0256 --- /dev/null +++ b/skill-tree/src/main/java/com/RDS/skilltree/utils/FetchAPI.java @@ -0,0 +1,36 @@ +package com.RDS.skilltree.utils; + +import com.RDS.skilltree.utils.RDSUser.Response; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; + +import java.util.concurrent.CompletableFuture; + +@Component +@Slf4j +public class FetchAPI { + private final RestTemplate restTemplate; + + public FetchAPI(RestTemplateBuilder restTemplateBuilder){ + restTemplate = restTemplateBuilder.build(); + } + + @Async + public CompletableFuture getRDSUserData(String userId) { + String url = String.format("https://api.realdevsquad.com/users/userId/%s", userId); + try{ + ResponseEntity response = restTemplate.getForEntity(url, Response.class); + Response result = response.getBody(); + return CompletableFuture.completedFuture(result); + }catch (Exception e){ + log.error("Error in calling the RDS backend, error : {}", e.getMessage(), e); + throw new HttpClientErrorException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage()); + } + } +} diff --git a/skill-tree/src/main/java/com/RDS/skilltree/utils/JWTUtils.java b/skill-tree/src/main/java/com/RDS/skilltree/utils/JWTUtils.java new file mode 100644 index 00000000..5945a439 --- /dev/null +++ b/skill-tree/src/main/java/com/RDS/skilltree/utils/JWTUtils.java @@ -0,0 +1,66 @@ +package com.RDS.skilltree.utils; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; +import org.springframework.stereotype.Component; + +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; + +@Component +@Slf4j +public class JWTUtils { + + @Value("${jwt.rds.public.key}") + private String publicRDSKeyString; + private KeyFactory keyFactory; + + @PostConstruct + public void init() throws NoSuchAlgorithmException { + keyFactory = KeyFactory.getInstance("RSA"); + } + /** + * Converts the given public key string to an RSAPublicKey object. + * + * @param publicKeyString the public key string to be converted + * @return the RSAPublicKey object converted from the public key string + * @throws Exception if there is an error during the conversion process + */ + private RSAPublicKey convertToRSAPublicKey(String publicKeyString) throws Exception { + publicKeyString = publicKeyString.replace("-----BEGIN PUBLIC KEY-----", "") + .replace("-----END PUBLIC KEY-----", "") + .replaceAll("\\s", ""); + + byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyString); + X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKeyBytes); + return (RSAPublicKey) keyFactory.generatePublic(keySpec); + } + + public String getRDSUserId(String token) throws Exception { + Claims claims = Jwts.parser().setSigningKey(convertToRSAPublicKey(publicRDSKeyString)).parseClaimsJws(token).getBody(); + String temp = claims.get("userId", String.class); + return temp; + } + + public boolean validateToken(String token) throws Exception { //TODO check for the case where token is expired + try { + Jwts.parser().setSigningKey(convertToRSAPublicKey(publicRDSKeyString)).parseClaimsJws(token); + return true; + } catch (InvalidKeySpecException e) { + throw new RuntimeException(e); + } catch (Exception e) { + throw new AuthenticationCredentialsNotFoundException("Invalid JWT"); + } + } + +} diff --git a/skill-tree/src/main/java/com/RDS/skilltree/utils/RDSUser/Picture.java b/skill-tree/src/main/java/com/RDS/skilltree/utils/RDSUser/Picture.java new file mode 100644 index 00000000..cf9a78e3 --- /dev/null +++ b/skill-tree/src/main/java/com/RDS/skilltree/utils/RDSUser/Picture.java @@ -0,0 +1,9 @@ +package com.RDS.skilltree.utils.RDSUser; + +import lombok.Data; + +@Data +public class Picture{ + private String publicId; + private String url; +} \ No newline at end of file diff --git a/skill-tree/src/main/java/com/RDS/skilltree/utils/RDSUser/Response.java b/skill-tree/src/main/java/com/RDS/skilltree/utils/RDSUser/Response.java new file mode 100644 index 00000000..1a363186 --- /dev/null +++ b/skill-tree/src/main/java/com/RDS/skilltree/utils/RDSUser/Response.java @@ -0,0 +1,9 @@ +package com.RDS.skilltree.utils.RDSUser; + +import lombok.Data; + +@Data +public class Response{ + private String message; + private User user; +} \ No newline at end of file diff --git a/skill-tree/src/main/java/com/RDS/skilltree/utils/RDSUser/Roles.java b/skill-tree/src/main/java/com/RDS/skilltree/utils/RDSUser/Roles.java new file mode 100644 index 00000000..155df3a3 --- /dev/null +++ b/skill-tree/src/main/java/com/RDS/skilltree/utils/RDSUser/Roles.java @@ -0,0 +1,9 @@ +package com.RDS.skilltree.utils.RDSUser; + +import lombok.Data; + +@Data +public class Roles{ + private boolean archived; + private boolean inDiscord; +} \ No newline at end of file diff --git a/skill-tree/src/main/java/com/RDS/skilltree/utils/RDSUser/User.java b/skill-tree/src/main/java/com/RDS/skilltree/utils/RDSUser/User.java new file mode 100644 index 00000000..e4d0c27e --- /dev/null +++ b/skill-tree/src/main/java/com/RDS/skilltree/utils/RDSUser/User.java @@ -0,0 +1,26 @@ +package com.RDS.skilltree.utils.RDSUser; + +import lombok.Data; + +@Data +public class User{ + private boolean incompleteUserDetails; + private String discordJoinedAt; + private String discordId; + private Roles roles; + private String last_name; + private long createdAt; + private String linkedinId; + private boolean nicknameSynced; + private Picture picture; + private long githubCreatedAt; + private String githubDisplayName; + private long updatedAt; + private String githubId; + private String company; + private String id; + private String designation; + private String twitterId; + private String first_name; + private String username; +} \ No newline at end of file