Skip to content

Commit

Permalink
Feature: Implementing JWT
Browse files Browse the repository at this point in the history
  • Loading branch information
FarukBraimo committed Jun 19, 2024
1 parent dba0e3c commit 826a04d
Show file tree
Hide file tree
Showing 14 changed files with 232 additions and 58 deletions.
12 changes: 12 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,18 @@
<version>8.7.0</version>
</dependency>

<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.5.0</version>
</dependency>

<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>

</dependencies>

<build>
Expand Down
29 changes: 29 additions & 0 deletions src/main/java/com/vodacom/falcon/config/SwaggerConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.vodacom.falcon.config;

import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@OpenAPIDefinition
@Slf4j
public class SwaggerConfig {
@Bean
public OpenAPI myOpenAPI() {
Contact contact = new Contact()
.email("faruquebraimo@gmail.com ")
.name("Faruque Braimo");

Info info = new Info()
.title("Falcon-API")
.version("1.0.0")
.contact(contact)
.description("Your travel assistant api");

return new OpenAPI().info(info);
}
}
46 changes: 13 additions & 33 deletions src/main/java/com/vodacom/falcon/config/security/CustomFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,71 +2,51 @@

import com.vodacom.falcon.model.User;
import com.vodacom.falcon.repository.UserRepository;
import com.vodacom.falcon.service.TokenService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
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.Base64;
import java.util.Objects;

import static com.vodacom.falcon.util.FalconDefaults.AUTHORIZATION;
import static com.vodacom.falcon.util.FalconDefaults.BASIC;

@Slf4j
@Component
@RequiredArgsConstructor
public class CustomFilter extends OncePerRequestFilter {
private final UserRepository userRepository;
private final PasswordEncoderProvider encoderProvider;
private final TokenService tokenService;


@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (isBasicAuth(request)) {
String base64 = this.getHeader(request)
.replace(BASIC, "");
String[] credentials = new String(Base64.getDecoder()
.decode(base64))
.split(":");

String username = credentials[0];
String pass = credentials[1];
String token = getToken(request);

if (token != null) {
var username = tokenService.validateToken(token);
User user = userRepository.findByUsername(username);

if (Objects.isNull(user)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("User not found!");
return;
}

if (encoderProvider.passwordEncoder().matches(user.getPassword(), pass)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Wrong Password");
return;
if (user != null) {
var authentication = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}

Authentication authentication = new UsernamePasswordAuthenticationToken(user, null, null);
SecurityContextHolder.getContext().setAuthentication(authentication);
}

filterChain.doFilter(request, response);
}

private boolean isBasicAuth(HttpServletRequest request) {
String header = getHeader(request);
return header != null && header.startsWith(BASIC);
}

private String getHeader(HttpServletRequest request) {
return request.getHeader(AUTHORIZATION);
private String getToken(HttpServletRequest request) {
String header = request.getHeader(AUTHORIZATION);
if (header == null) return null;
return header.replace("Bearer ", "");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

Expand All @@ -20,6 +24,12 @@
public class SecurityConfig {
@Autowired
private CustomFilter filter;
private static final String[] AUTH_WHITELIST = {
"falcon/auth/**",
"falcon/insight/",
"/v3/api-docs/**",
"/swagger-ui/**"
};

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
Expand All @@ -28,11 +38,22 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.authorizeHttpRequests(authorize -> authorize
.dispatcherTypeMatchers(FORWARD, ERROR)
.permitAll()
.requestMatchers("falcon/auth/**", "falcon/insight/**")
.requestMatchers(AUTH_WHITELIST)
.permitAll()
.anyRequest().denyAll()
.anyRequest()
.authenticated()
)
.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class)
.build();
}

@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
11 changes: 0 additions & 11 deletions src/main/java/com/vodacom/falcon/config/security/UserDetails.java

This file was deleted.

37 changes: 32 additions & 5 deletions src/main/java/com/vodacom/falcon/controller/AuthController.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
package com.vodacom.falcon.controller;


import com.vodacom.falcon.model.request.AuthLoginRequest;
import com.vodacom.falcon.model.request.UserRegistrationRequest;
import com.vodacom.falcon.model.response.TokenResponse;
import com.vodacom.falcon.service.AuthService;
import lombok.RequiredArgsConstructor;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
Expand All @@ -15,15 +23,34 @@

@RestController
@CrossOrigin(value = "*")
@RequiredArgsConstructor

@Slf4j
@RequestMapping("falcon/auth")
public class AuthController {
private final AuthService service;
@Autowired
private AuthService service;

@PostMapping("/signup")
public ResponseEntity<Void> signup(@RequestBody UserRegistrationRequest userRegistrationRequest) {
@PostMapping("/signUp")
@Operation(summary = "Sign Up", description = "For user registration")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", content = {@Content(schema = @Schema(implementation = String.class), mediaType = "application/json")}),
@ApiResponse(responseCode = "400", content = {@Content(schema = @Schema(implementation = String.class), mediaType = "application/json")}),
@ApiResponse(responseCode = "500", content = {@Content(schema = @Schema(implementation = String.class), mediaType = "application/json")})
})
public ResponseEntity<Void> signUp(@RequestBody @Valid UserRegistrationRequest userRegistrationRequest) {
service.createUser(userRegistrationRequest);
return new ResponseEntity<>(HttpStatus.ACCEPTED);
}

@PostMapping("/signIn")
@Operation(summary = "Sign In", description = "For user login")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", content = {@Content(schema = @Schema(implementation = String.class), mediaType = "application/json")}),
@ApiResponse(responseCode = "400", content = {@Content(schema = @Schema(implementation = String.class), mediaType = "application/json")}),
@ApiResponse(responseCode = "500", content = {@Content(schema = @Schema(implementation = String.class), mediaType = "application/json")})
})
public ResponseEntity<TokenResponse> signIn(@RequestBody @Valid AuthLoginRequest authLoginRequest) {
service.login(authLoginRequest);
return ResponseEntity.ok(service.login(authLoginRequest));
}
}
22 changes: 20 additions & 2 deletions src/main/java/com/vodacom/falcon/controller/InsightController.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
import com.vodacom.falcon.model.response.HistoricalEconomyInsightResponse;
import com.vodacom.falcon.model.response.InsightResponse;
import com.vodacom.falcon.service.InsightService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.Cacheable;
Expand All @@ -25,14 +31,26 @@ public class InsightController {
private final InsightService falconInsightService;

@GetMapping()
public ResponseEntity<InsightResponse> getInsight(@RequestParam("city") String city) {
@Operation(summary = "Get insight by city", description = "Should return population, gdp, exchange rates, weather forecast. The last 2 if the user is authenticated")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", content = {@Content(schema = @Schema(implementation = String.class), mediaType = "application/json")}),
@ApiResponse(responseCode = "400", content = {@Content(schema = @Schema(implementation = String.class), mediaType = "application/json")}),
@ApiResponse(responseCode = "500", content = {@Content(schema = @Schema(implementation = String.class), mediaType = "application/json")})
})
public ResponseEntity<InsightResponse> getInsight(@RequestParam("city") @Valid String city) {
InsightResponse response = falconInsightService.getInsight(city);
return new ResponseEntity<>(response, HttpStatus.OK);
}

@GetMapping("/historical")
@Operation(summary = "Get historical insights by country", description = "Should return population and gdp from 2012 to 2022 for given country")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", content = {@Content(schema = @Schema(implementation = String.class), mediaType = "application/json")}),
@ApiResponse(responseCode = "400", content = {@Content(schema = @Schema(implementation = String.class), mediaType = "application/json")}),
@ApiResponse(responseCode = "500", content = {@Content(schema = @Schema(implementation = String.class), mediaType = "application/json")})
})
@Cacheable(key = "#city", value = "historical")
public ResponseEntity<HistoricalEconomyInsightResponse> getHistoricalInsights(@RequestParam("city") String city) throws InterruptedException {
public ResponseEntity<HistoricalEconomyInsightResponse> getHistoricalInsights(@RequestParam("city") @Valid String city) {
HistoricalEconomyInsightResponse response = falconInsightService.getHistoricalInsights(city);
return new ResponseEntity<>(response, HttpStatus.OK);
}
Expand Down
10 changes: 9 additions & 1 deletion src/main/java/com/vodacom/falcon/model/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.time.LocalDateTime;
import java.util.Collection;
import java.util.UUID;

@Entity
Expand All @@ -22,11 +25,16 @@
@NoArgsConstructor
@Builder
@Table(name = "falcon_user")
public class User {
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
private String username;
private String password;
private LocalDateTime createdAt;

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.vodacom.falcon.model.request;

public record AuthLoginRequest(String username, String password) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.vodacom.falcon.model.response;

public record TokenResponse (String token) {
}
31 changes: 29 additions & 2 deletions src/main/java/com/vodacom/falcon/service/AuthService.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,33 @@

import com.vodacom.falcon.config.security.PasswordEncoderProvider;
import com.vodacom.falcon.model.User;
import com.vodacom.falcon.model.request.AuthLoginRequest;
import com.vodacom.falcon.model.request.UserRegistrationRequest;
import com.vodacom.falcon.model.response.TokenResponse;
import com.vodacom.falcon.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;

@Service
@RequiredArgsConstructor
@Slf4j
public class AuthService {
@RequiredArgsConstructor
public class AuthService implements UserDetailsService {

private final PasswordEncoderProvider encoderProvider;

private final UserRepository userRepository;
private final ApplicationContext context;
private final TokenService tokenService;

public void createUser(UserRegistrationRequest auth) {
User existingUser = userRepository.findByUsername(auth.username());
Expand All @@ -30,5 +43,19 @@ public void createUser(UserRegistrationRequest auth) {
.createdAt(LocalDateTime.now())
.build();
userRepository.save(user);

}

public TokenResponse login(AuthLoginRequest login) {
AuthenticationManager authenticationManager = context.getBean(AuthenticationManager.class);
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(login.username(), login.password());
Authentication auth = authenticationManager.authenticate(authToken);
var token = tokenService.generateToken((User) auth.getPrincipal());
return new TokenResponse(token);
}

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userRepository.findByUsername(username);
}
}
Loading

0 comments on commit 826a04d

Please sign in to comment.