Description :

Bonjour à tous,

Je travaille actuellement sur une application Angular qui communique avec une API Spring Boot pour l'authentification et la gestion des utilisateurs via JWT. Cependant, je rencontre des problèmes lors de la gestion de l'authentification avec le token JWT dans le frontend et dans le backend.

Contexte :

Frontend : Angular avec un JwtInterceptor pour ajouter le token JWT dans l'en-tête des requêtes.
Backend : Spring Boot avec Spring Security pour la génération et la validation des tokens JWT.
J'ai configuré une partie du code pour gérer l'inscription, la connexion, le changement de mot de passe et la suppression de compte. Voici un résumé de ce que j'ai fait jusqu'à présent.

Frontend (Angular)
JwtInterceptor : Récupère le token à partir du localStorage et l'ajoute à chaque requête HTTP.
AccountsService : Gère les appels API pour l'inscription, la connexion, et la gestion des utilisateurs. Le token est sauvegardé dans le localStorage.
Code JwtInterceptor.ts :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
 
@Injectable()
export class JwtInterceptor implements HttpInterceptor {
 
  constructor(private accountsService: AccountsService) { }
 
  intercept(req: HttpRequest<any>, next: HttpHandler) {
    const token = this.accountsService.getToken();
    if (token) {
      console.log('JWT Token:', token);
      const cloned = req.clone({
        headers: req.headers.set('Authorization', `Bearer ${token}`)
      });
      return next.handle(cloned);
    }
    return next.handle(req);
  }
}
 
accouts.service.ts
 
    export class AccountsService {
    private apiUrl = 'http://localhost:8080';
 
    constructor(private http: HttpClient) { }
    register(user: { username: string; password: string }): Observable<string> {
      return this.http.post<string>(`${this.apiUrl}/auth/register`, user, { responseType: 'text' as 'json' })
        .pipe(
          tap(response => console.log('User registered:', response)),
          catchError(this.handleError)
        );
    }
 
    login(credentials: { username: string; password: string }): Observable<string> {
      return this.http.post<string>(`${this.apiUrl}/auth/login`, credentials, { responseType: 'text' as 'json' })
        .pipe(
          tap(response => console.log('Login successful:', response)),
          catchError(this.handleError)
        );
    }
 
    saveToken(token: string) {
      localStorage.setItem('jwtToken', token);
    }
 
    getToken() {
      return localStorage.getItem('jwtToken');
    }
 
    logout() {
      localStorage.removeItem('jwtToken');
      console.log('User logged out, token removed');
    }
 
 
 }
backend

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
 
		@Configuration
	@EnableWebSecurity
	public class SecurityConfig {
 
	    @Bean
	    public BCryptPasswordEncoder passwordEncoder() {
	        return new BCryptPasswordEncoder();
	    }
 
	    @Bean
	    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
	        http
	            .csrf().disable()
	            .cors().and()  // Ajout du support CORS
	            .authorizeRequests()
	                .requestMatchers("/submit","/auth/register","/auth/login" ).permitAll()  // Public route
	                .anyRequest().authenticated()  // Authentification requise pour le reste
	            .and()
	            .addFilterBefore(jwtRequestFilter(), UsernamePasswordAuthenticationFilter.class);
 
	        return http.build();
	    }
 
	    @Bean
	    public JwtRequestFilter jwtRequestFilter() {
	        return new JwtRequestFilter();
	    }
 
	}
 
	@Component
	public class JwtTokenUtil {
 
	    private static final String SECRET_KEY = "monmot";
 
 
	    public String generateToken(String username) {
	        Map<String, Object> claims = new HashMap<>();
	        return Jwts.builder()
	            .setClaims(claims)
	            .setSubject(username)
	            .setIssuedAt(new Date(System.currentTimeMillis()))
	            .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) // 10 hours
	            .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
	            .compact();
	    }
 
	    public boolean validateToken(String token, String username) {
	        final String tokenUsername = extractUsername(token);
	        return (tokenUsername.equals(username) && !isTokenExpired(token));
	    }
 
	    public String extractUsername(String token) {
	        return extractAllClaims(token).getSubject();
	    }
 
	    private Claims extractAllClaims(String token) {
	        return Jwts.parser().setSigningKey(SECRET_KEY).build().parseSignedClaims(token).getPayload();
	    }
 
	    private boolean isTokenExpired(String token) {
	        return extractAllClaims(token).getExpiration().before(new Date());
	    }
	}
		public class JwtRequestFilter extends OncePerRequestFilter {
 
	    @Autowired
	    private JwtTokenUtil jwtTokenUtil;
 
	    @Override
	    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
	            throws ServletException, IOException {
	        final String authorizationHeader = request.getHeader("Authorization");
 
	        String username = null;
	        String jwt = null;
 
	        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
	            jwt = authorizationHeader.substring(7);
	            username = jwtTokenUtil.extractUsername(jwt);
	        }
 
	        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
	            UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
	                    username, null, new ArrayList<>());
	            usernamePasswordAuthenticationToken
	                    .setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
	            SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
	        }
	        chain.doFilter(request, response);
	    }
	}
AuthController : Gère l'authentification des utilisateurs et génère un token JWT lors de la connexion.
JwtTokenUtil : Crée et valide les tokens JWT.
JwtRequestFilter : Filtre chaque requête et valide le token JWT pour s'assurer que l'utilisateur est authentifié.
Mon problème :
Le problème que je rencontre est le suivant :

Le token JWT est correctement généré et envoyé au frontend lors de la connexion.
Cependant, lorsque j'envoie des requêtes authentifiées à partir d'Angular avec le token dans l'en-tête Authorization, il semble que le backend ne valide pas correctement le token. Les requêtes protégées renvoient une erreur 401 (Unauthorized).
J'ai vérifié le code et je ne vois pas d'erreur évidente dans la configuration de l'intercepteur Angular ni dans la partie backend.

Ce que j'ai déjà vérifié :
Le token est bien stocké et envoyé par l'intercepteur Angular.
Le JwtRequestFilter semble bien intercepté les requêtes, mais l'authentification échoue.
J'ai vérifié la configuration de Spring Security et les routes protégées sont bien spécifiées.
Ce que je recherche :
J'aimerais savoir si quelqu'un a déjà rencontré un problème similaire et pourrait me donner des pistes pour résoudre cette erreur 401 lors de la validation du token JWT dans Spring Boot. Est-ce que le problème vient de la configuration de Spring Security, de la signature du token, ou de l'envoi des requêtes depuis Angular ?

Merci d'avance pour votre aide !