Skip to content

Commit

Permalink
Merge pull request #17 from Arquisoft/feat/spring-api
Browse files Browse the repository at this point in the history
Added JWT authentication with Spring Security 3.2.2
  • Loading branch information
UO283615 authored Feb 9, 2024
2 parents 7390b7b + d31584e commit 72a2a8e
Show file tree
Hide file tree
Showing 23 changed files with 798 additions and 3 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
node_modules
coverage
docs/build
docs/build
.idea
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=Arquisoft_wiq_en2b&metric=coverage)](https://sonarcloud.io/summary/new_code?id=Arquisoft_wiq_en2b)

This is a base repo for the [Software Architecture course](http://arquisoft.github.io/) in [2023/2024 edition](https://arquisoft.github.io/course2324.html).

## Contributors
| Nombre | UO |
|---------------------------------|----------|
Expand All @@ -15,6 +15,7 @@ This is a base repo for the [Software Architecture course](http://arquisoft.gith
| Jorge Joaquín Gancedo Fernández | UO282161 |
| Sergio Quintana Fernández | UO288090 |
| Diego Villanueva Berros | UO283615 |
| Gonzalo Suárez Losada | UO283928 |

This repo is a basic application composed of several components.

Expand Down
32 changes: 32 additions & 0 deletions api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,22 @@
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>3.2.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.2.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>3.2.2</version>
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>3.0.2</version>
</dependency>

<dependency>
Expand All @@ -46,6 +58,26 @@
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.15.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.1</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.1</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.1</version>
</dependency>
</dependencies>

<build>
Expand Down
34 changes: 34 additions & 0 deletions api/src/main/java/lab/en2b/quizapi/auth/AuthController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package lab.en2b.quizapi.auth;

import jakarta.validation.Valid;
import lab.en2b.quizapi.auth.dtos.LoginDto;
import lab.en2b.quizapi.auth.dtos.RefreshTokenDto;
import lab.en2b.quizapi.auth.dtos.RegisterDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/auth")
public class AuthController {

@Autowired
private AuthService authService;

@PostMapping("/register")
public ResponseEntity<?> registerUser(@Valid @RequestBody RegisterDto registerRequest){
return authService.register(registerRequest);
}

@PostMapping("/login")
public ResponseEntity<?> loginUser(@Valid @RequestBody LoginDto loginRequest){
return authService.login(loginRequest);
}

@PostMapping("/refresh-token")
public ResponseEntity<?> refreshToken(@Valid @RequestBody RefreshTokenDto refreshTokenRequest){
return authService.refreshToken(refreshTokenRequest);
}
}
59 changes: 59 additions & 0 deletions api/src/main/java/lab/en2b/quizapi/auth/AuthService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package lab.en2b.quizapi.auth;

import lab.en2b.quizapi.auth.config.UserDetailsImpl;
import lab.en2b.quizapi.auth.dtos.*;
import lab.en2b.quizapi.auth.jwt.JwtUtils;
import lab.en2b.quizapi.commons.exceptions.TokenRefreshException;
import lab.en2b.quizapi.auth.dtos.RefreshTokenResponseDto;
import lab.en2b.quizapi.commons.user.User;
import lab.en2b.quizapi.commons.user.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Set;

@Service
@RequiredArgsConstructor
public class AuthService {
private final JwtUtils jwtUtils;
private final AuthenticationManager authenticationManager;
private final UserService userService;
/**
* Creates a session for a user. Throws an 401 unauthorized exception otherwise
* @param loginRequest the request containing the login info
* @return a response containing a fresh jwt token and a refresh token
*/
@Transactional
public ResponseEntity<JwtResponseDto> login(LoginDto loginRequest){
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginRequest.getEmail(), loginRequest.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authentication);
UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();

return ResponseEntity.ok(new JwtResponseDto(
jwtUtils.generateJwtTokenUserPassword(authentication),
userService.assignNewRefreshToken(userDetails.getId()),
userDetails.getId(),
userDetails.getUsername(),
userDetails.getEmail(),
userDetails.getStringRoles())
);
}

public ResponseEntity<?> register(RegisterDto registerRequest) {
userService.createUser(registerRequest,Set.of("user"));
return ResponseEntity.ok("User registered successfully!");
}

public ResponseEntity<?> refreshToken(RefreshTokenDto refreshTokenRequest) {
User user = userService.findByRefreshToken(refreshTokenRequest.getRefreshToken()).orElseThrow(() -> new TokenRefreshException(
"Refresh token is not in database!"));
return ResponseEntity.ok(new RefreshTokenResponseDto(jwtUtils.generateTokenFromEmail(user.getEmail()), user.obtainRefreshIfValid()));
}
}
84 changes: 84 additions & 0 deletions api/src/main/java/lab/en2b/quizapi/auth/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package lab.en2b.quizapi.auth.config;

import lab.en2b.quizapi.auth.jwt.JwtAuthFilter;
import lab.en2b.quizapi.commons.user.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
@Autowired
public UserService userService;
@Bean
public JwtAuthFilter authenticationJwtTokenFilter() {
return new JwtAuthFilter();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
// Configure CORS settings here
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
/**
* Security filter used for filtering all petitions, applying cors and asking for authentication among other things
* @param http the http request to filter
* @return the filtered request
* @throws Exception if any problem happens when filtering
*/
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, AuthenticationManager authenticationManager) throws Exception {
return http
.cors(Customizer.withDefaults())
.sessionManagement(configuration -> configuration.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authorize -> authorize
.requestMatchers(HttpMethod.POST,"/auth/**").permitAll()
.anyRequest().authenticated())
.csrf(AbstractHttpConfigurer::disable)
.authenticationManager(authenticationManager)
.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class)
.build();
//TODO: add exception handling
}

/**
* Builds the authorization manager taking into account password encoding
* @param http the http request to secure
* @return the newly created authentication manager
* @throws Exception if something goes wrong when creating the manager
*/
@Bean
public AuthenticationManager authManager(HttpSecurity http) throws Exception {
AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
authenticationManagerBuilder.userDetailsService(userService).passwordEncoder(passwordEncoder());
return authenticationManagerBuilder.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package lab.en2b.quizapi.auth.config;

import com.fasterxml.jackson.annotation.JsonIgnore;
import lab.en2b.quizapi.commons.user.User;
import lab.en2b.quizapi.commons.user.role.Role;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

@Getter
@AllArgsConstructor
public class UserDetailsImpl implements UserDetails {
private Long id;
private String username;
private String email;
@JsonIgnore
private String password;
private Collection<? extends GrantedAuthority> authorities;
public static UserDetailsImpl build(User user) {
List<GrantedAuthority> authorities = new ArrayList<>();
for(Role role : user.getRoles()){
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return new UserDetailsImpl(user.getId(),user.getUsername() , user.getEmail(), user.getPassword(), authorities);
}

@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
UserDetailsImpl user = (UserDetailsImpl) o;
return Objects.equals(id, user.id);
}

public List<String> getStringRoles() {
return getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());
}
}
22 changes: 22 additions & 0 deletions api/src/main/java/lab/en2b/quizapi/auth/dtos/JwtResponseDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package lab.en2b.quizapi.auth.dtos;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.List;

@AllArgsConstructor
@NoArgsConstructor
@Getter
public class JwtResponseDto {
private String token;
@JsonProperty("refresh_token")
private String refreshToken;
@JsonProperty("user_id")
private Long userId;
private String username;
private String email;
private List<String> roles;
}
16 changes: 16 additions & 0 deletions api/src/main/java/lab/en2b/quizapi/auth/dtos/LoginDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package lab.en2b.quizapi.auth.dtos;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;

@NoArgsConstructor
@AllArgsConstructor
@Data
public class LoginDto {
@NonNull
private String email;
@NonNull
private String password;
}
16 changes: 16 additions & 0 deletions api/src/main/java/lab/en2b/quizapi/auth/dtos/RefreshTokenDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package lab.en2b.quizapi.auth.dtos;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;

@AllArgsConstructor
@NoArgsConstructor
@Data
public class RefreshTokenDto {
@NonNull
@JsonProperty("refresh_token")
private String refreshToken;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package lab.en2b.quizapi.auth.dtos;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class RefreshTokenResponseDto {

private String token;
@JsonProperty("refresh_token")
private String refreshToken;

public RefreshTokenResponseDto(String accessToken, String refreshToken) {
this.token = accessToken;
this.refreshToken = refreshToken;
}

}
Loading

0 comments on commit 72a2a8e

Please sign in to comment.