diff --git a/.gitignore b/.gitignore index 2bc930286e5b36eeb7815448b6f6253b149387df..c882178d8f0665ff33136f7a26b880df3e5aab63 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,5 @@ build/ ### VS Code ### .vscode/ +requests.http + diff --git a/pom.xml b/pom.xml index baa2c8ab4cfc773f27b16995b182b54debf24e35..f056a3def032442e353770477484d031cfdb792b 100644 --- a/pom.xml +++ b/pom.xml @@ -29,11 +29,25 @@ <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> - <!-- <dependency>--> - <!-- <groupId>org.springframework.boot</groupId>--> - <!-- <artifactId>spring-boot-starter-security</artifactId>--> - <!-- </dependency>--> - + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-security</artifactId> + </dependency> + <dependency> + <groupId>io.jsonwebtoken</groupId> + <artifactId>jjwt-api</artifactId> + <version>0.11.5</version> + </dependency> + <dependency> + <groupId>io.jsonwebtoken</groupId> + <artifactId>jjwt-impl</artifactId> + <version>0.11.5</version> + </dependency> + <dependency> + <groupId>io.jsonwebtoken</groupId> + <artifactId>jjwt-jackson</artifactId> + <version>0.11.5</version> + </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> diff --git a/requests.http b/requests.http index 8945de921964eddda7b000b5f298928b01660fe7..d51fde8dc0b81d562777227dd8523f0330788ad3 100644 --- a/requests.http +++ b/requests.http @@ -1,2 +1,23 @@ ### Get all plants -GET http://localhost:8080/api/v1/plants \ No newline at end of file +GET http://localhost:8080/api/v1/plants +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJsdWthcy5rYXJzY2hAZ214LmRlIiwiaWF0IjoxNjk5ODczMDA4LCJleHAiOjE2OTk5NTk0MDh9.iU9gkWinFla3__ksuwLmKosevRXrrYlDdSQCPhNHxbM + +### Create account +POST http://localhost:8080/api/v1/auth/register +content-type: application/json + +{ + "email": "lukas.karsch@gmx.de", + "firstname": "Lukas", + "lastname": "Karsch", + "password": "myPassword123" +} + +### Authenticate +POST http://localhost:8080/api/v1/auth/authenticate +content-type: application/json + +{ + "email": "lukas.karsch@gmx.de", + "password": "12345678" +} diff --git a/src/main/java/hdm/mi/growbros/auth/AuthenticationRequest.java b/src/main/java/hdm/mi/growbros/auth/AuthenticationRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..0dfec3d2e88505610806d1a4bd7a972d222fc41e --- /dev/null +++ b/src/main/java/hdm/mi/growbros/auth/AuthenticationRequest.java @@ -0,0 +1,15 @@ +package hdm.mi.growbros.auth; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class AuthenticationRequest { + private String email; + String password; +} diff --git a/src/main/java/hdm/mi/growbros/auth/AuthenticationResponse.java b/src/main/java/hdm/mi/growbros/auth/AuthenticationResponse.java new file mode 100644 index 0000000000000000000000000000000000000000..a16fe46d690544679546018623b87b33ebf1b837 --- /dev/null +++ b/src/main/java/hdm/mi/growbros/auth/AuthenticationResponse.java @@ -0,0 +1,14 @@ +package hdm.mi.growbros.auth; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class AuthenticationResponse { + private String token; +} diff --git a/src/main/java/hdm/mi/growbros/auth/AuthenticationService.java b/src/main/java/hdm/mi/growbros/auth/AuthenticationService.java new file mode 100644 index 0000000000000000000000000000000000000000..acecf64d1eda1e83d56ab87035757732a12c1b44 --- /dev/null +++ b/src/main/java/hdm/mi/growbros/auth/AuthenticationService.java @@ -0,0 +1,55 @@ +package hdm.mi.growbros.auth; + +import hdm.mi.growbros.models.user.Role; +import hdm.mi.growbros.models.user.User; +import hdm.mi.growbros.models.user.UserRepository; +import hdm.mi.growbros.security.JwtService; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class AuthenticationService { + private final UserRepository repository; + private final PasswordEncoder passwordEncoder; + private final JwtService jwtService; + private final AuthenticationManager authenticationManager; + /** + * Ermöglicht es einen User zu erstellen und diesen in der Datenbank zu registrieren + * und daraus den Token zu erstellen. + * @param request + * @return + */ + public AuthenticationResponse register(RegisterRequest request) { + var user = User.builder() + .firstname(request.getFirstname()) + .lastname(request.getLastname()) + .email(request.getEmail()) + .password(passwordEncoder.encode(request.getPassword())) + .role(Role.USER) + .build(); + repository.save(user); + var jwtToken = jwtService.generateToken(user); + return AuthenticationResponse.builder() + .token(jwtToken) + .build(); + } + + public AuthenticationResponse authenticate(AuthenticationRequest request) { + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken( + request.getEmail(), + request.getPassword() + ) + ); + var user = repository.findByEmail(request.getEmail()) //wenn der Nutzername und Passwort korrekt sind wird ein Token generiert + .orElseThrow(); + var jwtToken = jwtService.generateToken(user); + return AuthenticationResponse.builder() + .token(jwtToken) + .build(); + } +} diff --git a/src/main/java/hdm/mi/growbros/auth/RegisterRequest.java b/src/main/java/hdm/mi/growbros/auth/RegisterRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..574edf0b297ab671621d1dc5a56923e291218de6 --- /dev/null +++ b/src/main/java/hdm/mi/growbros/auth/RegisterRequest.java @@ -0,0 +1,17 @@ +package hdm.mi.growbros.auth; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class RegisterRequest { + private String firstname; + private String lastname; + private String email; + private String password; +} diff --git a/src/main/java/hdm/mi/growbros/controllers/AuthenticationController.java b/src/main/java/hdm/mi/growbros/controllers/AuthenticationController.java new file mode 100644 index 0000000000000000000000000000000000000000..207ba55ff2cadb198d48476d886dd2d72cc9e629 --- /dev/null +++ b/src/main/java/hdm/mi/growbros/controllers/AuthenticationController.java @@ -0,0 +1,32 @@ +package hdm.mi.growbros.controllers; + +import hdm.mi.growbros.auth.AuthenticationRequest; +import hdm.mi.growbros.auth.AuthenticationResponse; +import hdm.mi.growbros.auth.AuthenticationService; +import hdm.mi.growbros.auth.RegisterRequest; +import lombok.RequiredArgsConstructor; +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("/api/v1/auth") +@RequiredArgsConstructor +public class AuthenticationController { + private final AuthenticationService service; + @PostMapping("/register") + public ResponseEntity<AuthenticationResponse> register( + @RequestBody RegisterRequest request + ) { + return ResponseEntity.ok(service.register(request)); + } + + @PostMapping("/authenticate") + public ResponseEntity<AuthenticationResponse> authenticate( + @RequestBody AuthenticationRequest request + ) { + return ResponseEntity.ok(service.authenticate(request)); + } +} diff --git a/src/main/java/hdm/mi/growbros/models/user/Role.java b/src/main/java/hdm/mi/growbros/models/user/Role.java new file mode 100644 index 0000000000000000000000000000000000000000..515b3ac4b547620929f60390f0777bc4309e8bf9 --- /dev/null +++ b/src/main/java/hdm/mi/growbros/models/user/Role.java @@ -0,0 +1,6 @@ +package hdm.mi.growbros.models.user; + +public enum Role { + USER, + ADMIN +} diff --git a/src/main/java/hdm/mi/growbros/models/user/User.java b/src/main/java/hdm/mi/growbros/models/user/User.java new file mode 100644 index 0000000000000000000000000000000000000000..fc6753f9bab6dc61f6b8acf3b776822f3b529ee6 --- /dev/null +++ b/src/main/java/hdm/mi/growbros/models/user/User.java @@ -0,0 +1,75 @@ +package hdm.mi.growbros.models.user; + +import jakarta.persistence.*; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "_user") +public class User implements UserDetails { + @Id + @GeneratedValue + private long id; + + private String firstname; + private String lastname; + + @Column(unique = true) + @Email + private String email; + + @Size(min = 8) + private String password; + + @Enumerated(EnumType.STRING) + private Role role; + + @Override + public Collection<? extends GrantedAuthority> getAuthorities() { + return List.of(new SimpleGrantedAuthority(role.name())); + } + + @Override + public String getPassword() { + return password; + } + + @Override + public String getUsername() { + return email; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/src/main/java/hdm/mi/growbros/models/user/UserRepository.java b/src/main/java/hdm/mi/growbros/models/user/UserRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..87b067d9ff512b4de55b60251b173f5c91428324 --- /dev/null +++ b/src/main/java/hdm/mi/growbros/models/user/UserRepository.java @@ -0,0 +1,9 @@ +package hdm.mi.growbros.models.user; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserRepository extends JpaRepository<User, Long> { + Optional<User> findByEmail(String email); +} diff --git a/src/main/java/hdm/mi/growbros/security/ApplicationConfig.java b/src/main/java/hdm/mi/growbros/security/ApplicationConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..5c2d13d458f73fb73036cc4e6dda1831e4b7dc0d --- /dev/null +++ b/src/main/java/hdm/mi/growbros/security/ApplicationConfig.java @@ -0,0 +1,45 @@ +package hdm.mi.growbros.security; + +import hdm.mi.growbros.models.user.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +@RequiredArgsConstructor +public class ApplicationConfig { + private final UserRepository repository; + @Bean + public UserDetailsService userDetailsService() { + return username -> repository.findByEmail(username) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); + } + + /** + * Fetcht die Benutzer Daten und decodiert das Passwort. + * @return + */ + @Bean + public AuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); + authProvider.setUserDetailsService(userDetailsService()); + authProvider.setPasswordEncoder(passwordEncoder()); + return authProvider; + } + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + return config.getAuthenticationManager(); + } + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/src/main/java/hdm/mi/growbros/security/JwtAuthenticationFilter.java b/src/main/java/hdm/mi/growbros/security/JwtAuthenticationFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..95afb44ac212e804570cb385b64396f24a7b42c6 --- /dev/null +++ b/src/main/java/hdm/mi/growbros/security/JwtAuthenticationFilter.java @@ -0,0 +1,51 @@ +package hdm.mi.growbros.security; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor //nutzt jedes final Feld +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtService jwtService; + private final UserDetailsService userDetailsService; + + @Override //überprüft bei jedem request ob ein Jwt dabei ist, wenn ja ob er gültig ist -> melde Nutzer an + protected void doFilterInternal(@NonNull HttpServletRequest request,@NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException { + final String authHeader = request.getHeader("Authorization"); + final String jwt; + final String userEmail; + if(authHeader == null ||!authHeader.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + jwt = authHeader.substring(7); //extract jwt + userEmail = jwtService.extractUsername(jwt);// extract userEmail from jwt token with the help of a class that can manipulate the jwt -> jwt Service + if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) { //case user is not yet authenticated + UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail); //get UserDetails from DataBase + if(jwtService.isTokenValid(jwt,userDetails)) { //case User is valid + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + userDetails, + null, + userDetails.getAuthorities() + ); + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); //set request details on authToken + SecurityContextHolder.getContext().setAuthentication(authToken);//update SecurityContextHolder + } + } + filterChain.doFilter(request,response); + } +} diff --git a/src/main/java/hdm/mi/growbros/security/JwtService.java b/src/main/java/hdm/mi/growbros/security/JwtService.java new file mode 100644 index 0000000000000000000000000000000000000000..13f8cb5f15722a0890c73e06401d789e9aa4f4cc --- /dev/null +++ b/src/main/java/hdm/mi/growbros/security/JwtService.java @@ -0,0 +1,84 @@ +package hdm.mi.growbros.security; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; + +import java.security.Key; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +@Service +public class JwtService { + private static final String SECRET_KEY = "222972303468637b5e6a2c467e5f6c4d4542632e4257797b6d6d7e28564d534a"; + public String extractUsername(String token) { + return extractClaim(token,Claims::getSubject); //Subject ist der Username(für Spring) bzw. bei uns email + } + + /** + * Extrahiert einen einzelnen Claim aus dem Jwt. + * @param token + * @param claimsResolver generische Funktion welche angibt welcher Claim aus dem Jwt extrahiert werden soll + * @return Ein einzelner Claim aud dem Jwt. + * @param <T> + */ + public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) { + final Claims claims = extractAllClaims(token); + return claimsResolver.apply(claims); + } + + public String generateToken(UserDetails userDetails) { + return generateToken(new HashMap<>(), userDetails); + } + + public String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) { + return Jwts + .builder() + .setClaims(extraClaims) + .setSubject(userDetails.getUsername()) + .setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24)) //token ist 24h gültig + .signWith(getSignInKey(), SignatureAlgorithm.HS256) + .compact(); + } + public boolean isTokenValid(String token, UserDetails userDetails) { + final String username = extractUsername(token); + return (username.equals(userDetails.getUsername())) && !isTokenExpired(token); + } + + private boolean isTokenExpired(String token) { + return extractExpiration(token).before(new Date()); + } + + private Date extractExpiration(String token) { + return extractClaim(token, Claims::getExpiration); + } + + /** + * extrahiert die "Claims" = Ansprüche die im Token selbst enthalten sind. + * Ein JWT besteht aus drei Teilen: dem Header, den Claims und der Signatur. + * Die Claims sind der Hauptteil, der die nützlichen Informationen enthält (z.B. iss = Aussteller des Tokens...) + * @param token + * @return alle Claims des Jwt + */ + private Claims extractAllClaims(String token) { + return Jwts + .parserBuilder() + .setSigningKey(getSignInKey()) + .build() + .parseClaimsJws(token) + .getBody(); + + } + + private Key getSignInKey() { + byte[] keyBytes = Decoders.BASE64.decode(SECRET_KEY); + return Keys.hmacShaKeyFor(keyBytes); //hmacShaKeyFor = Algorithmus + } +} diff --git a/src/main/java/hdm/mi/growbros/security/SecurityConfiguration.java b/src/main/java/hdm/mi/growbros/security/SecurityConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..3adcd66aa873cc3abd4760e0322aa21bf6b379dd --- /dev/null +++ b/src/main/java/hdm/mi/growbros/security/SecurityConfiguration.java @@ -0,0 +1,59 @@ +package hdm.mi.growbros.security; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationProvider; +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.HeadersConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; +import org.springframework.web.servlet.handler.HandlerMappingIntrospector; + +import static org.springframework.boot.autoconfigure.security.servlet.PathRequest.toH2Console; + +/** + * Configuration of the filter chain. + */ +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfiguration { + private final JwtAuthenticationFilter jwtAuthFilter; + private final AuthenticationProvider authenticationProvider; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http, MvcRequestMatcher.Builder mvc) throws Exception { + http + .csrf(csrf -> { + csrf.disable(); + csrf.ignoringRequestMatchers(toH2Console()); + }) + .authorizeHttpRequests((authorize) -> //whitelist + authorize + .requestMatchers(toH2Console()).permitAll() + .requestMatchers(mvc.pattern("/api/v1/auth/**")).permitAll() + .anyRequest().authenticated() + ) + .headers(headers -> headers.frameOptions( + HeadersConfigurer.FrameOptionsConfig::sameOrigin) + ) + .sessionManagement((sessionManagement) -> + sessionManagement + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) //stellt sicher dass man sich in jeder Session authentifizieren muss + ) + .authenticationProvider(authenticationProvider) + .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } + + @Bean + MvcRequestMatcher.Builder mvc(HandlerMappingIntrospector introspector) { + //see https://stackoverflow.com/questions/76809698/spring-security-method-cannot-decide-pattern-is-mvc-or-not-spring-boot-applicati + return new MvcRequestMatcher.Builder(introspector); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 443fc3744bfd09b45424b28773ae4ebb0c19888d..1132c3c9bcfa71dea06682653066523cad286353 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,6 +1,9 @@ -spring.datasource.url=jdbc:h2:mem:testdb +spring.datasource.url=jdbc:h2:~/growbros +spring.jpa.hibernate.ddl-auto=update spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=admin spring.datasource.password=admin spring.jpa.database-platform=org.hibernate.dialect.H2Dialect spring.h2.console.enabled=true + +app.jwtSecret = ffjsdafhjkfkafSecretjkdlfhw \ No newline at end of file