Salta el contingut

JWT (JSON Web Token) - Resum per Alumnes

1. Què és JWT?

Un JSON Web Token (JWT) és un estàndard obert de la indústria per transmetre informació entre un client i un servidor de manera segura. Es tracta d'un token codificat en format JSON que s'utilitza principalment per a l'autenticació i autorització en aplicacions web.

A diferència dels sistemes tradicionals que emmagatzemen dades de sessió al servidor, JWT implementa una autenticació sense estat (stateless), on tota la informació necessària es troba dins del propi token.

Avantatges de JWT

  • 🔒 Stateless: El servidor no ha de guardar informació de sessió
  • 📈 Escalable: Funciona perfectament en arquitectures de microserveis
  • 🛡 Segur: Les dades es signen digitalment per verificar la seva autenticitat
  • 📦 Auto-contingut: El token conté tota la informació necessària sobre l'usuari
  • 🌐 Multiplataforma: Funciona amb aplicacions web, mòbils i APIs

2. Estructura d'un JWT

Un JWT consta de tres parts principals, separades per punts (.):

header.payload.signature

2.1 Header (Capçalera)

Conté metadades sobre el token:

{
  "alg": "HS256",
  "typ": "JWT"
}
  • alg: Algoritme de signatura (HS256, RS256, etc.)
  • typ: Tipus de token (sempre JWT)

2.2 Payload (Càrrega)

Conté les dades de l'usuari (claims):

{
  "sub": "usuario123",
  "username": "joan.garcia",
  "email": "joan@example.com",
  "iat": 1704067200,
  "exp": 1704153600,
  "roles": ["user", "admin"]
}
  • sub: Identificador del subjecte (usuari)
  • username: Nom d'usuari
  • email: Correu electrònic
  • iat (issued at): Moment de creació del token (timestamp)
  • exp (expiration): Moment d'expiració del token
  • roles: Rols/permisos de l'usuari

2.3 Signature (Signatura)

La signatura es genera cifrando el header i payload amb una clau secreta:

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

Propòsit de la Signatura

Garantir que el token no ha estat modificat i verificar que la seva procedència és confiable.

Token complet exemple:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c3VhcmlvMTIzIiwidXNlcm5hbWUiOiJqb2FuIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c


3. Com funciona JWT en l'autenticació?

Flux d'autenticació

  1. Login: L'usuari envia les seves credencials (usuari i contrasenya) al servidor
  2. Validació: El servidor verifica les credencials contra la base de dades
  3. Generació del Token: Si són correctes, el servidor genera i signa el JWT
  4. Retorn del Token: El servidor envia el token al client
  5. Emmagatzematge: El client guarda el token (normalment a localStorage o sessionStorage)
  6. Peticions posteriors: El client inclou el token a cada petició al servidor
  7. Verificació: El servidor verifica la signatura del token per autoritzar la petició

Esquema del flux

Client                           Servidor
  |                                |
  |--- POST /login (user/pass)---->|
  |                                | Valida credencials
  |                                | Genera JWT
  |<--- JWT token -----------------|
  |                                |
  | Emmagatzema token              |
  |                                |
  |--- GET /api/datos + Token ---->|
  |                                | Verifica signatura
  |                                | Si és vàlid, processa
  |<--- Dades autoritzades --------|

4. Per què es fa servir JWT?

Casos d'ús principals

4.1 Single Page Applications (SPAs)

Les aplicacions Angular són SPAs que necessiten autenticació stateless per funcionar correctament. JWT és ideal perquè:

  • L'aplicació frontend pot guardar el token i fer peticions sense manteniment de sessió
  • El backend només valida el token sense guardar estat

4.2 APIs REST i Microserveis

En arquitectures modernes amb múltiples serveis:

  • Un token JWT pot ser vàlid en tots els serveis sense sincronització
  • Cada servei valida independentment el token

4.3 Single Sign-On (SSO)

Un usuari pot accedir a múltiples aplicacions amb un sol token JWT, evitant logins repetits.

4.4 Aplicacions Mòbils

Les aplicacions mòbils necessiten autenticació lleugera i JWT és perfecte per això.


5. Implementació amb Spring Boot i Angular

5.1 Backend: Spring Boot

Dependències (Maven)

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.12.3</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.12.3</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.12.3</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

Classe per Generar i Validar JWT

@Component
public class JwtUtils {

    @Value("${app.jwtSecret}")
    private String jwtSecret;

    @Value("${app.jwtExpirationMs}")
    private long jwtExpirationMs;

    // Generar JWT
    public String generateJwtToken(String username) {
        return Jwts.builder()
            .subject(username)
            .issuedAt(new Date())
            .expiration(new Date(System.currentTimeMillis() + jwtExpirationMs))
            .signWith(key(), SignatureAlgorithm.HS256)
            .compact();
    }

    // Obtenir usuari del token
    public String getUsernameFromJwtToken(String token) {
        return Jwts.parserBuilder()
            .setSigningKey(key())
            .build()
            .parseClaimsJws(token)
            .getBody()
            .getSubject();
    }

    // Validar token
    public boolean validateJwtToken(String authToken) {
        try {
            Jwts.parserBuilder()
                .setSigningKey(key())
                .build()
                .parseClaimsJws(authToken);
            return true;
        } catch (SecurityException e) {
            System.err.println("Invalid JWT signature: {}" + e);
        } catch (MalformedJwtException e) {
            System.err.println("Invalid JWT token: {}" + e);
        } catch (ExpiredJwtException e) {
            System.err.println("Expired JWT token: {}" + e);
        } catch (UnsupportedJwtException e) {
            System.err.println("Unsupported JWT token: {}" + e);
        } catch (IllegalArgumentException e) {
            System.err.println("JWT claims string is empty: {}" + e);
        }
        return false;
    }

    private Key key() {
        return Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtSecret));
    }
}

Controlador de Login

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private JwtUtils jwtUtils;

    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
        try {
            Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                    loginRequest.getUsername(),
                    loginRequest.getPassword()
                )
            );

            String jwt = jwtUtils.generateJwtToken(loginRequest.getUsername());
            return ResponseEntity.ok(new JwtResponse(jwt));

        } catch (BadCredentialsException e) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                .body("Credencials incorrectes");
        }
    }
}

Filtre per Validar JWT

@Component
public class AuthTokenFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtils jwtUtils;

    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                   HttpServletResponse response, 
                                   FilterChain filterChain) 
                                   throws ServletException, IOException {
        try {
            String jwt = parseJwt(request);

            if (jwt != null && jwtUtils.validateJwtToken(jwt)) {
                String username = jwtUtils.getUsernameFromJwtToken(jwt);
                // Establir l'autenticació al context de Spring Security
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                UsernamePasswordAuthenticationToken authentication = 
                    new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities()
                    );
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        } catch (Exception e) {
            System.err.println("Cannot set user authentication: {}" + e);
        }

        filterChain.doFilter(request, response);
    }

    private String parseJwt(HttpServletRequest request) {
        String headerAuth = request.getHeader("Authorization");

        if (headerAuth != null && headerAuth.startsWith("Bearer ")) {
            return headerAuth.substring(7);
        }

        return null;
    }
}

Configuració de Spring Security

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private AuthTokenFilter authTokenFilter;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .exceptionHandling()
            .and()
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
                .antMatchers("/api/auth/**").permitAll()
                .anyRequest().authenticated();

        http.addFilterBefore(authTokenFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

Configuració d'Entorn

Afegeix aquestes propietats a application.properties:

app.jwtSecret=your-secret-key-here-change-this-in-production
app.jwtExpirationMs=3600000

5.2 Frontend: Angular

Interceptor de HTTP

Els interceptors d'Angular permeten afegir automàticament el token a totes les peticions HTTP:

import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable()
export class JwtInterceptor implements HttpInterceptor {

    constructor(private authService: AuthService) { }

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

        // Obtenir el token emmagatzemat
        const token = this.authService.getToken();

        if (token) {
            // Clonar la petició i afegir el header d'autenticació
            req = req.clone({
                setHeaders: {
                    Authorization: `Bearer ${token}`
                }
            });
        }

        return next.handle(req);
    }
}

Servei d'Autenticació

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, BehaviorSubject } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable({
    providedIn: 'root'
})
export class AuthService {

    private apiUrl = 'http://localhost:8080/api/auth';
    private tokenKey = 'auth_token';

    private currentUserSubject: BehaviorSubject<any>;
    public currentUser: Observable<any>;

    constructor(private http: HttpClient) {
        this.currentUserSubject = new BehaviorSubject<any>(
            this.getUserFromToken()
        );
        this.currentUser = this.currentUserSubject.asObservable();
    }

    // Login
    login(username: string, password: string): Observable<any> {
        return this.http.post<any>(`${this.apiUrl}/login`, {
            username,
            password
        }).pipe(
            tap(response => {
                // Guardar el token
                localStorage.setItem(this.tokenKey, response.token);
                this.currentUserSubject.next(this.getUserFromToken());
            })
        );
    }

    // Logout
    logout() {
        localStorage.removeItem(this.tokenKey);
        this.currentUserSubject.next(null);
    }

    // Obtenir el token emmagatzemat
    getToken(): string | null {
        return localStorage.getItem(this.tokenKey);
    }

    // Verificar si l'usuari està autenticat
    isAuthenticated(): boolean {
        const token = this.getToken();
        return token != null;
    }

    // Decodificar el token per obtenir l'usuari
    private getUserFromToken(): any {
        const token = this.getToken();
        if (token) {
            const payload = token.split('.')[1];
            const decoded = JSON.parse(atob(payload));
            return decoded;
        }
        return null;
    }
}

Component de Login

import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from '../../services/auth.service';

@Component({
    selector: 'app-login',
    templateUrl: './login.component.html',
    styleUrls: ['./login.component.css']
})
export class LoginComponent {

    username: string = '';
    password: string = '';
    loading: boolean = false;
    submitted: boolean = false;
    error: string = '';

    constructor(
        private authService: AuthService,
        private router: Router
    ) { }

    login() {
        this.submitted = true;
        this.error = '';
        this.loading = true;

        this.authService.login(this.username, this.password)
            .subscribe(
                data => {
                    this.router.navigate(['/dashboard']);
                },
                error => {
                    this.error = 'Usuari o contrasenya incorrectes';
                    this.loading = false;
                }
            );
    }
}

Protector de Rutes (Route Guard)

import { Injectable } from '@angular/core';
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { AuthService } from '../services/auth.service';

@Injectable({
    providedIn: 'root'
})
export class AuthGuard implements CanActivate {

    constructor(
        private router: Router,
        private authService: AuthService
    ) { }

    canActivate(
        route: ActivatedRouteSnapshot,
        state: RouterStateSnapshot
    ) {
        const isAuthenticated = this.authService.isAuthenticated();

        if (isAuthenticated) {
            return true;
        }

        // Redirigir al login
        this.router.navigate(['/login'], { 
            queryParams: { returnUrl: state.url } 
        });
        return false;
    }
}

Configuració de Rutes amb Guards

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from './guards/auth.guard';
import { LoginComponent } from './components/login/login.component';
import { DashboardComponent } from './components/dashboard/dashboard.component';

const routes: Routes = [
    { path: 'login', component: LoginComponent },
    { 
        path: 'dashboard', 
        component: DashboardComponent,
        canActivate: [AuthGuard]
    },
    { path: '', redirectTo: '/dashboard', pathMatch: 'full' }
];

@NgModule({
    imports: [RouterModule.forRoot(routes)],
    exports: [RouterModule]
})
export class AppRoutingModule { }

Configuració d'Interceptors al Module

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { JwtInterceptor } from './interceptors/jwt.interceptor';

@NgModule({
    declarations: [
        AppComponent
    ],
    imports: [
        BrowserModule,
        AppRoutingModule,
        HttpClientModule
    ],
    providers: [
        {
            provide: HTTP_INTERCEPTORS,
            useClass: JwtInterceptor,
            multi: true
        }
    ],
    bootstrap: [AppComponent]
})
export class AppModule { }

6. Consideracions de Seguretat

6.1 Emmagatzematge del Token

Llocs on Guardar el Token

Opció Avantatges Desavantatges
localStorage Fàcil accés Vulnerable a XSS attacks
sessionStorage Més segur que localStorage Limitat a la sessió del navegador
Cookies HttpOnly Secure Millor seguretat Més complex de configurar

6.2 Expiració del Token

Sempre establir un temps d'expiració curt (15 minuts a 1 hora). Per a sessions més llargues, implementar:

  • Refresh Tokens: Token separate amb expiració més llarga
  • Token Renovation: Generar un nou token quan l'anterior expira

Exemple de Refresh Token

// Al login, retornar tant accessToken com refreshToken
{
  "accessToken": "eyJhbGc...",
  "refreshToken": "eyJhbGc...",
  "expiresIn": 3600
}

6.3 Clau Secreta

Protecció de la Clau Secreta

  • 🔒 Mantenir la clau secreta del costat del servidor
  • 🔑 Usar claus fortes i complexes
  • 🔄 Rotar les claus periodicamente
  • 📁 Usar variables d'entorn (no commitar al Git)

6.4 HTTPS

Protocol Segur

Sempre usar HTTPS en producció per protegir els tokens durant la transmissió.

6.5 Validació

  • Sempre validar la signatura del token
  • Verificar que no ha expirat
  • Validar els claims esperats

7. Comparativa: JWT vs Sessions Tradicionals

Aspecte JWT Sessions Tradicionals
Estat Stateless Amb estat (servidor)
Escalabilitat Millor per a distribuïts Necessita sincronització
Mida Més gran per petició Més petit
Revocació Difícil fins expiració Immediata
SPAs Ideal Menys òptim
Microserveis Perfecte Complex

8. Resum

Punts Claus de JWT

JWT és un estàndard modern i segur per a autenticació en aplicacions web contemporànies. Els seus avantatges en escalabilitat i compatibilitat amb SPAs i microserveis el fan la solució ideal per a Angular i Spring Boot.

La implementació requereix atenció en seguretat:

  1. 🔒 Guardar els tokens de forma segura
  2. 🛡 Usar HTTPS sempre
  3. ⌛ Implementar expiració i refresh tokens
  4. 🔑 Protegir la clau secreta
  5. ✅ Validar sempre els tokens al servidor

9. Recursos Addicionals

Enllaços Útils