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

Added JWT authentication with Spring Security 3.2.2 #17

Merged
merged 41 commits into from
Feb 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
493d7ad
chore: removed .idea
Toto-hitori Feb 2, 2024
95d1df7
Merge branch 'develop' into feat/spring-api
Toto-hitori Feb 3, 2024
6a2d124
feat: auth controller
Toto-hitori Feb 3, 2024
f5b7791
feat: register endpoint
Toto-hitori Feb 3, 2024
2126f72
feat: non null checks in LoginDto
Toto-hitori Feb 3, 2024
51e37fe
feat: refresh token endpoint
Toto-hitori Feb 3, 2024
d9376be
feat: mocked security config
Toto-hitori Feb 3, 2024
c107683
feat: user and role tables
Toto-hitori Feb 3, 2024
575f170
feat: jwt auth filter
Toto-hitori Feb 3, 2024
c65dcd7
refactor: jwt auth filter
Toto-hitori Feb 3, 2024
4dc7fc7
feat: added user to role
Toto-hitori Feb 3, 2024
627ed44
feat: jwt expiration
Toto-hitori Feb 3, 2024
f068a59
Merge branch 'develop' of https://github.com/Arquisoft/wiq_en2b into …
Feb 4, 2024
e510b82
chore: name added in README.md
Feb 4, 2024
d1d4878
chore: name added in README.md
Feb 4, 2024
64026e4
Merge remote-tracking branch 'origin/feat/spring-api' into feat/sprin…
Feb 4, 2024
204096b
feat: email as jwt subject
Toto-hitori Feb 5, 2024
72eb50a
chore: commented authManager
Toto-hitori Feb 5, 2024
35c5655
feat: dummy login implementation
Toto-hitori Feb 5, 2024
4a93b0d
chore: cleaned code
Toto-hitori Feb 5, 2024
12e95d3
chore: commented code
Toto-hitori Feb 5, 2024
6563fb7
chore: unused imports
Toto-hitori Feb 5, 2024
ca736c4
feat: register implementation
Toto-hitori Feb 5, 2024
c4d0b28
chore: removed unused methods
Toto-hitori Feb 5, 2024
2a0a6ac
chore: cleaned code
Toto-hitori Feb 5, 2024
c9ce864
feat: register implementation
Toto-hitori Feb 5, 2024
6f569f5
chore: removed unused methods
Toto-hitori Feb 5, 2024
b4e767b
Merge remote-tracking branch 'origin/feat/spring-api' into feat/sprin…
Toto-hitori Feb 6, 2024
9f0097f
fix!: cors filter
Toto-hitori Feb 6, 2024
633222f
chore: moved RoleRepository
Toto-hitori Feb 6, 2024
70f7215
feat: database settings -> environment variables
Toto-hitori Feb 6, 2024
ac9d652
fix: added missing request body
Toto-hitori Feb 6, 2024
a3164b2
fix: added missing dependencies
Toto-hitori Feb 6, 2024
e99a2e1
refactor: JwtService -> JwtUtils
Toto-hitori Feb 6, 2024
7820bf5
feat: updated pom dependencies
Toto-hitori Feb 6, 2024
5a0c70c
feat: refresh token
Toto-hitori Feb 6, 2024
c9091c3
feat: exception handling
Toto-hitori Feb 6, 2024
6b18c5c
refactor: moved user folder
Toto-hitori Feb 6, 2024
8dd1e5c
feat: finished auth
Toto-hitori Feb 6, 2024
c3947c7
chore: used same logger as CustomControllerAdvice
Toto-hitori Feb 7, 2024
d31584e
Merge branch 'initial-modifications' into feat/spring-api
Toto-hitori Feb 7, 2024
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
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