From f164000cc8d561991aee4d35b7aeda0892897a05 Mon Sep 17 00:00:00 2001 From: Blandine Bajard <83599148+BlandineBajard@users.noreply.github.com> Date: Wed, 19 Jan 2022 15:18:21 +0100 Subject: [PATCH] securite token jwt --- pom.xml | 9 + .../fr/organizee/OrganizeeApplication.java | 36 +++- .../controller/ContactController.java | 6 + .../controller/MembreController.java | 43 +++- .../organizee/controller/TeamController.java | 6 + .../java/fr/organizee/dto/JsonWebToken.java | 18 ++ src/main/java/fr/organizee/dto/MembreDto.java | 53 +++++ .../exception/ExistingUsernameException.java | 15 ++ .../InvalidCredentialsException.java | 15 ++ .../exception/InvalidJWTException.java | 17 ++ src/main/java/fr/organizee/model/Membre.java | 30 ++- src/main/java/fr/organizee/model/Role.java | 16 ++ .../repository/MembreRepository.java | 8 + .../fr/organizee/security/JwtTokenFilter.java | 46 +++++ .../organizee/security/JwtTokenProvider.java | 185 ++++++++++++++++++ .../organizee/security/WebSecurityConfig.java | 72 +++++++ .../fr/organizee/service/MembreService.java | 45 +++++ .../organizee/service/MembreServiceImpl.java | 69 +++++++ .../service/UserDetailsServiceImpl.java | 40 ++++ 19 files changed, 719 insertions(+), 10 deletions(-) create mode 100644 src/main/java/fr/organizee/dto/JsonWebToken.java create mode 100644 src/main/java/fr/organizee/dto/MembreDto.java create mode 100644 src/main/java/fr/organizee/exception/ExistingUsernameException.java create mode 100644 src/main/java/fr/organizee/exception/InvalidCredentialsException.java create mode 100644 src/main/java/fr/organizee/exception/InvalidJWTException.java create mode 100644 src/main/java/fr/organizee/model/Role.java create mode 100644 src/main/java/fr/organizee/security/JwtTokenFilter.java create mode 100644 src/main/java/fr/organizee/security/JwtTokenProvider.java create mode 100644 src/main/java/fr/organizee/security/WebSecurityConfig.java create mode 100644 src/main/java/fr/organizee/service/MembreService.java create mode 100644 src/main/java/fr/organizee/service/MembreServiceImpl.java create mode 100644 src/main/java/fr/organizee/service/UserDetailsServiceImpl.java diff --git a/pom.xml b/pom.xml index 32eb628..dbaf653 100644 --- a/pom.xml +++ b/pom.xml @@ -25,6 +25,15 @@ org.springframework.boot spring-boot-starter-jdbc + + org.springframework.boot + spring-boot-starter-security + + + io.jsonwebtoken + jjwt + 0.9.1 + org.springframework.boot spring-boot-starter-web diff --git a/src/main/java/fr/organizee/OrganizeeApplication.java b/src/main/java/fr/organizee/OrganizeeApplication.java index e1323b4..6e2aaa4 100644 --- a/src/main/java/fr/organizee/OrganizeeApplication.java +++ b/src/main/java/fr/organizee/OrganizeeApplication.java @@ -1,13 +1,47 @@ package fr.organizee; +import fr.organizee.model.Membre; +import fr.organizee.model.Role; +import fr.organizee.service.MembreService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +import java.util.ArrayList; +import java.util.Arrays; @SpringBootApplication -public class OrganizeeApplication { +public class OrganizeeApplication implements CommandLineRunner { + + @Autowired + private MembreService membreService; public static void main(String[] args) { SpringApplication.run(OrganizeeApplication.class, args); } + /** + * Ceci est un Bean, un composant + * Méthode de Hachage + * Bcrypt est un algorithme de hachage considé comme le plus sûr. + * bcrypt est un algorithme de hashage unidirectionnel, + * vous ne pourrez jamais retrouver le mot de passe sans connaitre à la fois le grain de sel, + * la clé et les différentes passes que l'algorithme à utiliser. + * Voir le site pour effectuer un test + * + * @return + */ + @Bean + public BCryptPasswordEncoder bCryptPasswordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Override + public void run(String... args) throws Exception { + + } } + diff --git a/src/main/java/fr/organizee/controller/ContactController.java b/src/main/java/fr/organizee/controller/ContactController.java index 27d0d32..5186d12 100644 --- a/src/main/java/fr/organizee/controller/ContactController.java +++ b/src/main/java/fr/organizee/controller/ContactController.java @@ -8,6 +8,7 @@ import fr.organizee.repository.TeamRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import javax.persistence.EntityNotFoundException; @@ -23,6 +24,7 @@ public class ContactController { private ContactRepository contactRepo; @GetMapping(value = "/{id}") + @PreAuthorize("hasRole('ROLE_PARENT') or hasRole('ROLE_ENFANT')") public ResponseEntity findById(@PathVariable int id){ Optional contact = null; try @@ -36,6 +38,7 @@ public class ContactController { } @GetMapping(value = "team/{team_id}") + @PreAuthorize("hasRole('ROLE_PARENT') or hasRole('ROLE_ENFANT')") public ResponseEntity findByTeamId(@PathVariable int team_id){ List contacts = null; try @@ -49,6 +52,7 @@ public class ContactController { } @PostMapping(value="/add") + @PreAuthorize("hasRole('ROLE_PARENT') or hasRole('ROLE_ENFANT')") public ResponseEntity addContact(@RequestBody Contact contact){ Contact resultContact = null; try { @@ -61,6 +65,7 @@ public class ContactController { } @PutMapping("/update/{id}") + @PreAuthorize("hasRole('ROLE_PARENT') or hasRole('ROLE_ENFANT')") public ResponseEntity updateContact(@RequestBody Contact contact, @PathVariable Integer id) throws Exception { Contact resultContact = null; try { @@ -74,6 +79,7 @@ public class ContactController { } @DeleteMapping(value = "/delete/{id}") + @PreAuthorize("hasRole('ROLE_PARENT')") public ResponseEntity deleteContact(@PathVariable int id){ try { contactRepo.delete(contactRepo.getById(id)); diff --git a/src/main/java/fr/organizee/controller/MembreController.java b/src/main/java/fr/organizee/controller/MembreController.java index dc0edf7..e80f339 100644 --- a/src/main/java/fr/organizee/controller/MembreController.java +++ b/src/main/java/fr/organizee/controller/MembreController.java @@ -1,17 +1,24 @@ package fr.organizee.controller; +import fr.organizee.dto.JsonWebToken; +import fr.organizee.dto.MembreDto; +import fr.organizee.exception.ExistingUsernameException; +import fr.organizee.exception.InvalidCredentialsException; import fr.organizee.model.Membre; //import fr.organizee.model.Team; import fr.organizee.repository.MembreRepository; //import fr.organizee.repository.TeamRepository; +import fr.organizee.service.MembreService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import javax.persistence.EntityNotFoundException; import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; /* toto */ @RestController @@ -22,6 +29,9 @@ public class MembreController { @Autowired private MembreRepository membreRepo; + @Autowired + private MembreService membreService; + // @Autowired // private TeamRepository teamRepo; @@ -36,6 +46,7 @@ public class MembreController { } @GetMapping(value = "/all") + @PreAuthorize("hasRole('ROLE_PARENT') or hasRole('ROLE_ENFANT')") public ResponseEntity getAll(){ List liste = null; try @@ -48,6 +59,13 @@ public class MembreController { return ResponseEntity.status(HttpStatus.OK).body(liste); } + @GetMapping("/admin/all") + @PreAuthorize("hasRole('ROLE_PARENT')") + public List getAllAdminUsers() { + return membreService.findAllUsers().stream().map(appUser -> new MembreDto(appUser.getEmail(), appUser.getRoleList())).collect(Collectors.toList()); + + } + // @GetMapping(value = "/team/all") // public ResponseEntity getAllTeam(){ // List liste = null; @@ -62,6 +80,7 @@ public class MembreController { // } @GetMapping(value = "/{id}") + @PreAuthorize("hasRole('ROLE_PARENT') or hasRole('ROLE_ENFANT')") public ResponseEntity findById(@PathVariable int id){ Optional membre = null; try @@ -82,11 +101,12 @@ public class MembreController { // } @DeleteMapping(value = "/delete/{id}") + @PreAuthorize("hasRole('ROLE_PARENT')") public ResponseEntity deleteMembre(@PathVariable int id){ try { membreRepo.delete(membreRepo.getById(id)); //membreRepo.deleteById(id); - return ResponseEntity.status(HttpStatus.OK).body("Membre effacée !"); + return ResponseEntity.status(HttpStatus.OK).body("Membre effacé !"); } catch (EntityNotFoundException e) { @@ -94,19 +114,26 @@ public class MembreController { } } - @PostMapping(value="/add", produces="application/json", consumes="application/json") - public ResponseEntity addMembre(@RequestBody Membre membre){ - Membre resultMembre = null; + @PostMapping("/sign-up") + public ResponseEntity signUp(@RequestBody Membre membre) { try { - resultMembre = membreRepo.saveAndFlush(membre); - } catch (Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage()); + return ResponseEntity.ok(new JsonWebToken(membreService.signup(membre))); + } catch (ExistingUsernameException ex) { + return ResponseEntity.badRequest().build(); } + } - return ResponseEntity.status(HttpStatus.CREATED).body(resultMembre); + @PostMapping("/sign-in") + public ResponseEntity signIn(@RequestBody Membre membre) { + try { + return ResponseEntity.ok(new JsonWebToken(membreService.signin(membre.getEmail(), membre.getPassword()))); + } catch (InvalidCredentialsException ex) { + return ResponseEntity.badRequest().build(); + } } @PutMapping("/update/{id}") + @PreAuthorize("hasRole('ROLE_PARENT')") public ResponseEntity updateMembre(@RequestBody Membre membre, @PathVariable Integer id) throws Exception { Membre resultMembre = null; try { diff --git a/src/main/java/fr/organizee/controller/TeamController.java b/src/main/java/fr/organizee/controller/TeamController.java index aa01915..f8766ca 100644 --- a/src/main/java/fr/organizee/controller/TeamController.java +++ b/src/main/java/fr/organizee/controller/TeamController.java @@ -6,6 +6,7 @@ import fr.organizee.repository.TeamRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import javax.persistence.EntityNotFoundException; @@ -33,6 +34,7 @@ public class TeamController { // Récupération de toutes les teams @GetMapping(value = "/all") + @PreAuthorize("hasRole('ROLE_PARENT')") public ResponseEntity getAllTeam(){ List liste = null; try @@ -46,6 +48,7 @@ public class TeamController { } @GetMapping(value = "/{id}") + @PreAuthorize("hasRole('ROLE_PARENT') or hasRole('ROLE_ENFANT')") public ResponseEntity findTeamById(@PathVariable int id){ Optional liste = null; try @@ -59,6 +62,7 @@ public class TeamController { } @PostMapping(value="/add", produces="application/json", consumes="application/json") + @PreAuthorize("hasRole('ROLE_PARENT')") public ResponseEntity addTeam(@RequestBody Team team){ Team resultTeam = null; try { @@ -71,6 +75,7 @@ public class TeamController { } @PutMapping("/update/{id}") + @PreAuthorize("hasRole('ROLE_PARENT')") public ResponseEntity updateTeam(@RequestBody Team team, @PathVariable Integer id) throws Exception { Team resultTeam = null; try { @@ -84,6 +89,7 @@ public class TeamController { } @DeleteMapping(value = "/delete/{id}") + @PreAuthorize("hasRole('ROLE_PARENT')") public ResponseEntity deleteTeam(@PathVariable int id){ try { teamRepo.delete(teamRepo.getById(id)); diff --git a/src/main/java/fr/organizee/dto/JsonWebToken.java b/src/main/java/fr/organizee/dto/JsonWebToken.java new file mode 100644 index 0000000..5f608a6 --- /dev/null +++ b/src/main/java/fr/organizee/dto/JsonWebToken.java @@ -0,0 +1,18 @@ +package fr.organizee.dto; + +/** + * Classe spécifique DTO (Data Transfer Object) qui retourne un Jeton au format JSON (REST response) + * + */ +public class JsonWebToken { + private final String token; + + public JsonWebToken(String token) { + this.token = token; + } + + public String getToken() { + return token; + } +} + diff --git a/src/main/java/fr/organizee/dto/MembreDto.java b/src/main/java/fr/organizee/dto/MembreDto.java new file mode 100644 index 0000000..c62999c --- /dev/null +++ b/src/main/java/fr/organizee/dto/MembreDto.java @@ -0,0 +1,53 @@ +package fr.organizee.dto; + +import java.util.List; + +import com.sun.istack.NotNull; + +import fr.organizee.model.Role; + +/** + * Specifique : AppUser DTO permet de renvoyer un User sans le mot de passe (REST response). + */ +public class MembreDto { + + private Long id; + private String email; + private List roleList; + + public MembreDto() { + } + + public MembreDto(@NotNull String email) { + this(email, null); + } + + public MembreDto(@NotNull String email, List roleList) { + this.email = email; + this.roleList = roleList; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public List getRoleList() { + return roleList; + } + + public void setRoleList(List roleList) { + this.roleList = roleList; + } +} diff --git a/src/main/java/fr/organizee/exception/ExistingUsernameException.java b/src/main/java/fr/organizee/exception/ExistingUsernameException.java new file mode 100644 index 0000000..85e3bd8 --- /dev/null +++ b/src/main/java/fr/organizee/exception/ExistingUsernameException.java @@ -0,0 +1,15 @@ +package fr.organizee.exception; + +/** + * Classe personnalisée pour gérer un message si l'utilisateur (User) existe en Base de données + */ +public class ExistingUsernameException extends Exception { + + private static final long serialVersionUID = 1L; + + @Override + public String getMessage() + { + return "Désolé, l'utilisateur existe déjà en base de données !"; + } +} diff --git a/src/main/java/fr/organizee/exception/InvalidCredentialsException.java b/src/main/java/fr/organizee/exception/InvalidCredentialsException.java new file mode 100644 index 0000000..82f1155 --- /dev/null +++ b/src/main/java/fr/organizee/exception/InvalidCredentialsException.java @@ -0,0 +1,15 @@ +package fr.organizee.exception; + +/** + * Specific exception that should be thrown when user credentials are not valid. + */ +public class InvalidCredentialsException extends Exception { + + private static final long serialVersionUID = -6483691380297851921L; + + @Override + public String getMessage() + { + return "L'accréditation est invalide !"; + } +} \ No newline at end of file diff --git a/src/main/java/fr/organizee/exception/InvalidJWTException.java b/src/main/java/fr/organizee/exception/InvalidJWTException.java new file mode 100644 index 0000000..314385f --- /dev/null +++ b/src/main/java/fr/organizee/exception/InvalidJWTException.java @@ -0,0 +1,17 @@ +package fr.organizee.exception; + +/** + * Specific exception that should be thrown when a JWT has an invalid format. + */ +public class InvalidJWTException extends Exception { + + private static final long serialVersionUID = -6546999838071338632L; + + @Override + public String getMessage() + { + return "Le format JWT est invalide !"; + } + +} + diff --git a/src/main/java/fr/organizee/model/Membre.java b/src/main/java/fr/organizee/model/Membre.java index 36f2bd8..5733712 100644 --- a/src/main/java/fr/organizee/model/Membre.java +++ b/src/main/java/fr/organizee/model/Membre.java @@ -2,9 +2,11 @@ package fr.organizee.model; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.sun.istack.NotNull; import javax.persistence.*; import java.time.LocalDate; +import java.util.List; @Entity @@ -15,8 +17,19 @@ public class Membre { private String nom; private String prenom; private LocalDate dateNaissance; + + @NotNull + @Column(nullable = false) private String email; + + @NotNull + @Column(nullable = false) private String password; + + @ElementCollection(fetch = FetchType.EAGER) + @Enumerated(EnumType.STRING) + private List roleList; + private String isAdmin; private String couleur; private String smiley; @@ -31,7 +44,7 @@ public class Membre { public Membre() { } - public Membre(String nom, String prenom, LocalDate dateNaissance, String email, String password, String isAdmin, String couleur, String smiley, Team team) { + public Membre(String nom, String prenom, LocalDate dateNaissance, @NotNull String email, @NotNull String password, String isAdmin, String couleur, String smiley, Team team, List roleList) { this.nom = nom; this.prenom = prenom; this.dateNaissance = dateNaissance; @@ -41,8 +54,16 @@ public class Membre { this.couleur = couleur; this.smiley = smiley; this.team = team; + this.roleList=roleList; } + public Membre(@NotNull String email, @NotNull String password, List roleList) { + this.email = email; + this.password = password; + this.roleList=roleList; + } + + public int getId() { return id; } @@ -109,6 +130,13 @@ public class Membre { this.smiley = smiley; } + public List getRoleList() { + return roleList; + } + public void setRoleList(List roleList) { + this.roleList = roleList; + } + @Override public String toString() { return "Membre{" + diff --git a/src/main/java/fr/organizee/model/Role.java b/src/main/java/fr/organizee/model/Role.java new file mode 100644 index 0000000..b183ca2 --- /dev/null +++ b/src/main/java/fr/organizee/model/Role.java @@ -0,0 +1,16 @@ +package fr.organizee.model; + +import org.springframework.security.core.GrantedAuthority; + +/** + * User possible roles. + */ +public enum Role implements GrantedAuthority { + + ROLE_PARENT, ROLE_ENFANT; + + @Override + public String getAuthority() { + return name(); + } +} diff --git a/src/main/java/fr/organizee/repository/MembreRepository.java b/src/main/java/fr/organizee/repository/MembreRepository.java index a8aafe8..f431da3 100644 --- a/src/main/java/fr/organizee/repository/MembreRepository.java +++ b/src/main/java/fr/organizee/repository/MembreRepository.java @@ -4,7 +4,15 @@ import fr.organizee.model.Membre; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.Optional; + @Repository public interface MembreRepository extends JpaRepository { Membre findByNom(String nom); + + Optional findByEmail(String email); + + boolean existsByEmail(String email); + + void deleteByEmail(String email); } diff --git a/src/main/java/fr/organizee/security/JwtTokenFilter.java b/src/main/java/fr/organizee/security/JwtTokenFilter.java new file mode 100644 index 0000000..93ab540 --- /dev/null +++ b/src/main/java/fr/organizee/security/JwtTokenFilter.java @@ -0,0 +1,46 @@ +package fr.organizee.security; + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.http.HttpStatus; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; + +import fr.organizee.exception.InvalidJWTException; + + +/** + * Filtre specifique en charge d'analyser la requête HTTP qui arrive vers notre Serveur et qui doit + * contenir un JWT valide. + */ +public class JwtTokenFilter extends OncePerRequestFilter { + private JwtTokenProvider jwtTokenProvider; + + public JwtTokenFilter(JwtTokenProvider jwtTokenProvider) { + this.jwtTokenProvider = jwtTokenProvider; + } + + @Override + protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { + String token = jwtTokenProvider.resolveToken(httpServletRequest); + try { + if (token != null && jwtTokenProvider.validateToken(token)) { + Authentication auth = jwtTokenProvider.getAuthentication(token); + SecurityContextHolder.getContext().setAuthentication(auth); + } + } catch (InvalidJWTException ex) { + // permet de garantir que le AppClient n'est pas authentifié + SecurityContextHolder.clearContext(); + httpServletResponse.sendError(HttpStatus.BAD_REQUEST.value(), "JWT invalide !"); + return; + } + + filterChain.doFilter(httpServletRequest, httpServletResponse); + } +} diff --git a/src/main/java/fr/organizee/security/JwtTokenProvider.java b/src/main/java/fr/organizee/security/JwtTokenProvider.java new file mode 100644 index 0000000..9ec508d --- /dev/null +++ b/src/main/java/fr/organizee/security/JwtTokenProvider.java @@ -0,0 +1,185 @@ +package fr.organizee.security; + +import java.util.Base64; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import javax.annotation.PostConstruct; +import javax.servlet.http.HttpServletRequest; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.core.JsonProcessingException; + +import fr.organizee.exception.InvalidJWTException; +import fr.organizee.model.Role; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; + +/** + * JWT : classe utilitaire chargée de fournir le Jeton (Token) et les vérifications + */ +@Component +public class JwtTokenProvider { + + // on récupère le secret dans notre fichier application.properties + @Value("${security.jwt.token.secret-key:secret-key}") + private String secretKey; + + // ici on met la valeur par défaut + @Value("${security.jwt.token.expire-length:3600000}") + private long validityInMilliseconds = 3600000; // 1h pour être pénard + + @Autowired + private UserDetailsService userDetailsService; + + /** + * Cette méthode d'initialisation s'exécute avant le constructeur + * Elle encode notre code secret en base64 pour la transmission dans le header + */ + @PostConstruct + protected void init() { + secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes()); + } + + /** + * Methode qui crée le Token avec : + * username comme un champ "sub", + * User Role comme champ "auth" + * "iat" comme date du jour , + * "exp" as now date + validity time. + * claims = les droits + struture : + HEADER : Algo + Type de Token + { + "alg": "HS256", + "typ": "JWT" + } + + PAYLOAD : data + { + "sub": "pbouget", + "auth": [ + "ROLE_ADMIN", + "ROLE_CREATOR", + "ROLE_READER" + ], + "iat": 1589817421, + "exp": 1589821021 + } + + Signature : + + Signature avec code secret : + + HMACSHA256( + base64UrlEncode(header) + "." + + base64UrlEncode(payload), + 03888dd6ceb88c3fee410a70802fb93d483fd52d70349d8f7e7581ae346cf658 + ) + + JWT génèrer avec cette info : + header = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. + payload = eyJzdWIiOiJwYm91Z2V0IiwiYXV0aCI6WyJST0xFX0FETUlOIiwiUk9MRV9DUkVBVE9SIiwiUk9MRV9SRUFERVIiXSwiaWF0IjoxNTg5ODE3NDIxLCJleHAiOjE1ODk4MjEwMjF9. + signature = lrKQIkrCzNMwzTN-hs_EdoYYxrb59sAlku7nmaml0vk + + vérifier sur https://jwt.io + + * @param email the user email. + * @param roles the user roles. + * @return the created JWT as String. + * @throws JsonProcessingException + */ + public String createToken(String email, List roles){ + + Claims claims = Jwts.claims().setSubject(email); + claims.put("auth", roles.stream().map(s -> new SimpleGrantedAuthority(s.getAuthority())).filter(Objects::nonNull).collect(Collectors.toList())); + + System.out.println("claims = "+claims); + // claims = {sub=pbouget, auth=[ROLE_ADMIN, ROLE_CREATOR, ROLE_READER]} + Date now = new Date(); + Date validity = new Date(now.getTime() + validityInMilliseconds); + + String leToken = Jwts.builder()// + .setClaims(claims)// le username avec les roles ou setPayload() + .setIssuedAt(now)// 1589817421 pour le 18 mai 2020 à 17 heure 57 + .setExpiration(validity)// 1589821021 même date avec 1 heure de plus + .signWith(SignatureAlgorithm.HS256, secretKey) // la signature avec la clef secrête. + .compact(); // concatène l'ensemble pour construire une chaîne + System.out.println(leToken); // pour test cela donne ceci + /* + site pour convertir une date en millisecondes : http://timestamp.fr/? + site structure du jeton : https://www.vaadata.com/blog/fr/jetons-jwt-et-securite-principes-et-cas-dutilisation/ + site jwt encoder / décoder : https://jwt.io/ + eyJhbGciOiJIUzI1NiJ9. + eyJzdWIiOiJwYm91Z2V0IiwiYXV0aCI6W3siYXV0aG9yaXR5IjoiUk9MRV9BRE1JTiJ9LHsiYXV0aG9yaXR5IjoiUk9MRV9DUkVBVE9SIn0seyJhdXRob3JpdHkiOiJST0xFX1JFQURFUiJ9XSwiaWF0IjoxNTg5ODE2OTIyLCJleHAiOjE1ODk4MjA1MjJ9. + Cn4_UTjZ2UpJ32FVT3Bd1-VN8K62DVBHQbWiK6MNZ04 + + */ + // https://www.codeflow.site/fr/article/java__how-to-convert-java-object-to-from-json-jackson + + return leToken; + } + + /** + * Methode qui retourne un objet Authentication basé sur JWT. + * @param token : le token pour l'authentification. + * @return the authentication si Username est trouvé. + */ + public Authentication getAuthentication(String token) { + UserDetails userDetails = userDetailsService.loadUserByUsername(getEmail(token)); + return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); + } + + /** + * Methode qui extrait le userName du JWT. + * @param token : Token a analyser. + * @return le UserName comme chaîne de caractères. + */ + public String getEmail(String token) { + + return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject(); + } + + /** + * Méthode qui récupère la requete HTTP. + * L'entête doit contenir un champ d'autorisation ou JWT ajoute le token après le mot clef Bearer. + * @param requete : la requête à tester. + * @return le JWT depuis l'entête HTTP. + */ + public String resolveToken(HttpServletRequest requeteHttp) { + String bearerToken = requeteHttp.getHeader("Authorization"); + if (bearerToken != null && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + return null; + } + + /** + * Methode qui v�rifie que JWT est valide. + * La signature doit �tre correcte et la dur�e de validit� du Token doit �tre apr�s "now" (maintenant) + * @param token : Token � valider + * @return True si le Token est valide sinon on lance l'exception InvalidJWTException. + * @throws InvalidJWTException + */ + public boolean validateToken(String token) throws InvalidJWTException { + try { + Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); + return true; + } catch (JwtException | IllegalArgumentException e) { + throw new InvalidJWTException(); + } + } +} + diff --git a/src/main/java/fr/organizee/security/WebSecurityConfig.java b/src/main/java/fr/organizee/security/WebSecurityConfig.java new file mode 100644 index 0000000..358ce32 --- /dev/null +++ b/src/main/java/fr/organizee/security/WebSecurityConfig.java @@ -0,0 +1,72 @@ +package fr.organizee.security; + + +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.BeanIds; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +/** + * Configuration de S�curit� globale pour notre REST API. + */ +@Configuration +@EnableGlobalMethodSecurity(prePostEnabled = true) +@EnableWebSecurity +public class WebSecurityConfig extends WebSecurityConfigurerAdapter { + + @Autowired + private JwtTokenProvider jwtTokenProvider; + + @Bean(name = BeanIds.AUTHENTICATION_MANAGER) + @Override + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + + /** + * Methode qui configure la s�curit� HTTP. + * @param http the HttpSecurity object to configure. + * @throws Exception + */ + @Override + protected void configure(HttpSecurity http) throws Exception { + + // Disable CSRF (Cross Site Request Forgery comme votre Token sera stock� dans le session storage) + http.cors(); + + http.csrf().disable() + .authorizeRequests() + .antMatchers("/**").permitAll() // accessible sans besoin de s'authentifier + .and() + .authorizeRequests() + .antMatchers("/membres/sign-in").permitAll() // se connecter + .antMatchers("/membres/sign-up").permitAll() // s'inscrire + .antMatchers("membres/all").hasAuthority("ROLE_PARENT") // uniquement pour le r�le admin + .anyRequest().authenticated(); // tout le reste est autoris� par un utilisateur authentifi� + // Appliquer un filtre avec le token pour toutes requ�tes HTTP + http.addFilterBefore(new JwtTokenFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class); + + } + + /** + * Methode qui configure la s�curit� web. + * Utilis� pour interdire l'acc�s à certains r�pertoires. + * @param web : WebSecurity + * @throws Exception + */ + @Override + public void configure(WebSecurity web) throws Exception { + web.ignoring().antMatchers("/resources/**"); + } +} + + + diff --git a/src/main/java/fr/organizee/service/MembreService.java b/src/main/java/fr/organizee/service/MembreService.java new file mode 100644 index 0000000..4000c67 --- /dev/null +++ b/src/main/java/fr/organizee/service/MembreService.java @@ -0,0 +1,45 @@ +package fr.organizee.service; + +import java.util.List; +import java.util.Optional; + +import org.springframework.stereotype.Service; + +import fr.organizee.exception.ExistingUsernameException; +import fr.organizee.exception.InvalidCredentialsException; +import fr.organizee.model.Membre; + +@Service +public interface MembreService { + + /** + * Methode qui permet à un utilisateur de se connecter. + * @param email : nom de l'utilisateur. + * @param password : mot de passe de l'utilisateur. + * @returnun JWT si credentials est valide, throws InvalidCredentialsException otherwise. + * @throws InvalidCredentialsException + */ + String signin(String email, String password) throws InvalidCredentialsException; + + /** + * Methode qui permet de s'inscrire. + * @param membre nouvel utilisateur. + * @return un JWT si user n'existe pas déjà ! + * @throws ExistingUsernameException + */ + String signup(Membre membre) throws ExistingUsernameException; + + /** + * Methode qui retourne tous les utilisateurs de la bd + * @return the list of all application users. + */ + List findAllUsers(); + + /** + * Methode qui retourne un utilisateur à partir de son username + * @param email the username to look for. + * @return an Optional object containing user if found, empty otherwise. + */ + Optional findUserByEmail(String email); +} + diff --git a/src/main/java/fr/organizee/service/MembreServiceImpl.java b/src/main/java/fr/organizee/service/MembreServiceImpl.java new file mode 100644 index 0000000..6304d86 --- /dev/null +++ b/src/main/java/fr/organizee/service/MembreServiceImpl.java @@ -0,0 +1,69 @@ +package fr.organizee.service; + +import java.util.List; +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; + +import fr.organizee.exception.ExistingUsernameException; +import fr.organizee.exception.InvalidCredentialsException; +import fr.organizee.model.Membre; +import fr.organizee.repository.MembreRepository; +import fr.organizee.security.JwtTokenProvider; + +@Service +public class MembreServiceImpl implements MembreService { + + @Autowired + private MembreRepository membreRepository; // permet communication avec la BD + + @Autowired + private BCryptPasswordEncoder passwordEncoder; // permet l'encodage du mot de passe + + @Autowired + private JwtTokenProvider jwtTokenProvider; // permet la fourniture du Jeton (Token) + + @Autowired + private AuthenticationManager authenticationManager; // gestionnaire d'authentification + + + /** + * Permet de se connecter en encodant le mot de passe avec génération du token. + */ + @Override + public String signin(String email, String password) throws InvalidCredentialsException { + try { + authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(email, password)); + return jwtTokenProvider.createToken(email, membreRepository.findByEmail(email).get().getRoleList()); + } catch (AuthenticationException e) { + throw new InvalidCredentialsException(); + } + } + + @Override + public String signup(Membre membre) throws ExistingUsernameException { + if (!membreRepository.existsByEmail(membre.getEmail())) { + Membre membreToSave = new Membre(membre.getEmail(), passwordEncoder.encode(membre.getPassword()), membre.getRoleList()); + membreRepository.save(membreToSave); + return jwtTokenProvider.createToken(membre.getEmail(), membre.getRoleList()); + } else { + throw new ExistingUsernameException(); + } + } + + @Override + public List findAllUsers() { + return membreRepository.findAll(); + } + + @Override + public Optional findUserByEmail(String email) { + return membreRepository.findByEmail(email); + } +} + diff --git a/src/main/java/fr/organizee/service/UserDetailsServiceImpl.java b/src/main/java/fr/organizee/service/UserDetailsServiceImpl.java new file mode 100644 index 0000000..3ea4151 --- /dev/null +++ b/src/main/java/fr/organizee/service/UserDetailsServiceImpl.java @@ -0,0 +1,40 @@ +package fr.organizee.service; + +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.User; +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 fr.organizee.model.Membre; +import fr.organizee.repository.MembreRepository; + +@Service +public class UserDetailsServiceImpl implements UserDetailsService { + + @Autowired + private MembreRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String email) { + final Optional user = userRepository.findByEmail(email); + + if (!user.isPresent()) { + throw new UsernameNotFoundException("utilisateur '" + email + "' introuvable"); + } + + return User + .withUsername(email) + .password(user.get().getPassword()) + .authorities(user.get().getRoleList()) + .accountExpired(false) + .accountLocked(false) + .credentialsExpired(false) + .disabled(false) + .build(); + } +} +