Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Authorisation and integrate sign up for users #22

Merged
merged 24 commits into from
Mar 10, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a34ded2
added authorisation for skill tree
DashDeipayan Sep 22, 2023
d5b44fc
added services
DashDeipayan Sep 22, 2023
20a8924
Merge branch 'develop' into jpa-entity-rel
DashDeipayan Sep 22, 2023
b146108
Resolving PR comments
vikhyat187 Nov 11, 2023
af96355
Making the RSA key generation a static variable
vikhyat187 Nov 14, 2023
4bdce6a
Merge branch 'develop' into jpa-entity-rel
vikhyat187 Nov 27, 2023
5b6889d
Resolving comments
vikhyat187 Nov 27, 2023
3b15a20
Merge upstream changes
vikhyat187 Nov 27, 2023
a6953be
Making the dependencies injected by constructor, to make class testable
vikhyat187 Nov 28, 2023
eab8fb9
Updated logging and added null check
vikhyat187 Dec 2, 2023
afce972
Merge branch 'develop' into jpa-entity-rel
Ajeyakrishna-k Dec 8, 2023
e7b7d80
Merge branch 'develop' into jpa-entity-rel
vikhyat187 Dec 9, 2023
1a77019
resolving merge conflicts
vikhyat187 Dec 13, 2023
5dea02e
Uncommenting files which got commented due to mistake
vikhyat187 Dec 13, 2023
35516cc
Merge branch 'develop' into jpa-entity-rel
vikhyat187 Dec 16, 2023
56a1da9
Resolving merge conflicts
vikhyat187 Dec 19, 2023
9fe423f
Merge branch 'jpa-entity-rel' of https://github.com/Real-Dev-Squad/sk…
vikhyat187 Dec 19, 2023
0386e58
Merge branch 'develop' into jpa-entity-rel
vikhyat187 Dec 20, 2023
80f9988
Added removed dependencies
vikhyat187 Dec 21, 2023
b3e394a
Removing the extra space from the directory name
vikhyat187 Dec 22, 2023
ecfdf46
Merge branch 'develop' into jpa-entity-rel
vikhyat187 Dec 23, 2023
e42abdf
resolving merge conflicts
vikhyat187 Dec 23, 2023
d20539a
Merge conflicts
vikhyat187 Dec 23, 2023
a7ec2c7
Merge branch 'jpa-entity-rel' of https://github.com/Real-Dev-Squad/sk…
vikhyat187 Dec 23, 2023
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
1 change: 1 addition & 0 deletions skill-tree /.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@ build/

### VS Code ###
.vscode/
dev.env
DashDeipayan marked this conversation as resolved.
Show resolved Hide resolved
43 changes: 43 additions & 0 deletions skill-tree /pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
<name>skill-tree</name>
<description>skill tree project</description>
<properties>
<start-class>com.RDS.skilltree.SkillTreeApplication</start-class>
<java.version>17</java.version>
<io.jsonwebtoken.version>0.11.2</io.jsonwebtoken.version>
</properties>
<dependencies>
<dependency>
Expand Down Expand Up @@ -56,11 +58,52 @@
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
<version>17.0.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${io.jsonwebtoken.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${io.jsonwebtoken.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${io.jsonwebtoken.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -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());
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
vikhyat187 marked this conversation as resolved.
Show resolved Hide resolved

@Autowired
private FetchAPI fetchAPI;

private final UserService userService;
vikhyat187 marked this conversation as resolved.
Show resolved Hide resolved

@Autowired
private UserRepository userRepository;

public JWTAuthenticationFilter(UserService userService){
this.userService = userService;
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
vikhyat187 marked this conversation as resolved.
Show resolved Hide resolved
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<Response> userResponse = fetchAPI.getRDSUserData(rdsUserId);
CompletableFuture.anyOf(userResponse).join();
Response rdsUserResponse = userResponse.get();
Copy link
Member

Choose a reason for hiding this comment

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

get() method throws an exception if the CompletableFuture does not complete successfully. This means that if the getRDSUserData() method fails, the entire request will fail.

Copy link
Contributor

@vikhyat187 vikhyat187 Oct 31, 2023

Choose a reason for hiding this comment

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

should we handle this using timeouts? @iamitprakash, so that we don't perform the operation for too long and return TimeoutException in case of service taking higher time?

Copy link
Contributor

Choose a reason for hiding this comment

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

If there is any exception in making the call to the RDS backend we return an exception.

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;
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,27 @@
package com.RDS.skilltree;

import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;


@SpringBootApplication
@OpenAPIDefinition(
info = @Info(
title = "Skill Tree",
version = "1.0.0",
description = "Skill Tree"
),
tags = @Tag(
name = "Skill Tree",
description = "Skill Tree"
)
)
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<UserModel, UUID> {
Optional<UserModel> findByRdsUserId(String rdsUserId);

Boolean existsByRdsUserId(String rdsUserId);
}
36 changes: 36 additions & 0 deletions skill-tree /src/main/java/com/RDS/skilltree/utils/FetchAPI.java
vikhyat187 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -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<Response> getRDSUserData(String userId) {
Copy link
Member

Choose a reason for hiding this comment

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

Use a circuit breaker. A circuit breaker is a pattern that can be used to prevent cascading failures. It works by monitoring the number of failures that occur when calling a service. If the number of failures exceeds a certain threshold, the circuit breaker will trip and all subsequent calls to the service will fail immediately. This will prevent the service from being overloaded and causing other services to fail.

Copy link
Contributor

Choose a reason for hiding this comment

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

+1

Copy link
Contributor

Choose a reason for hiding this comment

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

Can I push these changes in the coming PR?

Copy link
Contributor

Choose a reason for hiding this comment

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

Since this is a bigger change, can I push in the coming PR, as @bhtibrewal and @heyrandhir changes were blocked on this PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

Here as the auth itself will not be succeded, we should try retrying to fetch the user data from the RDS backend, as circuit breaker might lead to storing of dummy data in the skill tree users db.

String url = String.format("https://api.realdevsquad.com/users/userId/%s", userId);
try{
ResponseEntity<Response> response = restTemplate.getForEntity(url, Response.class);
Response result = response.getBody();
return CompletableFuture.completedFuture(result);
}catch (Exception e){
Copy link
Member

Choose a reason for hiding this comment

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

this line throws a HttpClientErrorException with the status code HttpStatus.INTERNAL_SERVER_ERROR if any exception is thrown while calling the restTemplate.getForEntity() method. However, it does not catch specific exceptions, such as SocketTimeoutException or TimeoutException. This means that if the restTemplate.getForEntity() method times out, the getRDSUserData() method will throw a HttpClientErrorException with the status code HttpStatus.INTERNAL_SERVER_ERROR, even though the error is not related to the HTTP client.

Copy link
Contributor

Choose a reason for hiding this comment

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

As user is interacting with skill tree backend, if there is an error in calling any 3rd party service (RDS backend), I think from user POV its internal server error right?
@iamitprakash

Copy link
Contributor

Choose a reason for hiding this comment

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

check the timeout value

Copy link
Contributor

Choose a reason for hiding this comment

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

Here the socket exception, might not happen timeout can happen, try adding the timeout to the website backend call, to avoid waiting for the backend for long times

log.error("Error in calling the RDS backend, error : {}", e.getMessage(), e);
throw new HttpClientErrorException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage());
}
}
}
Loading