====== Securización de APIs con Spring Boot y JWT ======

En este apartado añadiremos la configuración y código necesario para securizar con JWT una API desarrollada con Spring Boot. El punto de partida puede ser cualquier proyecto Spring Boot que funcione como una API REST.

Todo el apartado se basa en proyecto [[https://github.com/codeandcoke/spring-web/tree/master/library-api-sec|library-api-sec]] que hay disponible en el repositorio de [[https://github.com/codeandcoke/spring-web|spring-web]]

Securizando la API, el flujo al utilizarla se modifica un poco. Ahora nuestra intención es que las operaciones solamente puedan ser invocadas por usuarios identificados (exceptuando los endpoints para registrarse y obtener el token). Asi, nuestra API quedará protegida de forma que solamente los usuarios registrados podan hacer uso de ella (también podemos disponer de operaciones abiertas y otras securizadas, e incluso mediante el uso de roles). Para aquellas operaciones que se encuentren securizadas, será necesario añadir un //Bearer// token (JWT) como valor para la cabecera //Authorization//. Y para ello se deberá disponer de un usuario y contraseña.

En la API de ejemplo, añadiremos también un endpoint para el registro de usuario. Asi, podremos registrarnos, solicitar el token e invocar a cualquiera de las operaciones securizadas de esta API. Todo aquello siguiendo el flujo que se muestra a continuación:

<figure>
{{ jwt.png?550 }}
<caption>Flujo petición API securizada con JWT (fuente: https://www.positronx.io)</caption></figure>

===== JWT =====

[[https://jwt.io/|JSON Web Token]]





===== Añadir las dependencias de Spring Boot Security al proyecto =====

<code xml>
. . .

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
<groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt</artifactId>
  <version>0.9.1</version>
</dependency>

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

. . .
</code>

===== Crear los certificados para firmar y decodificar el JWT =====

En este paso crearemos los certificados que se utilizarán para firmar (privado) y para comprobar el token (público).

Más adelante, los añadiremos el fichero de propiedades **application.properties** para cargarlos cuando hagan falta en el fichero de configuración global de la API **LibraryConfig.java**.

<code bash> 
# Crear un par de claves rsa (keypair.pem)
openssl genrsa -out keypair.pem 2048

# Crear un certificado público (app.pub)
openssl rsa -in keypair.pem -pubout -out app.pub

# Crear un certificado privado (app.key)
openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in keypair.pem -out app.key
</code>


===== Configuración global de seguridad para la API: ApiConfiguration =====

<code java>
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(
        // securedEnabled = true,
        // jsr250Enabled = true,
        prePostEnabled = true)
public class LibraryConfig extends WebSecurityConfigurerAdapter {

    @Value("${key.public}")
    RSAPublicKey key;

    @Value("${key.private}")
    RSAPrivateKey priv;

    @Autowired
    private LibraryUserDetailsService libraryUserDetailsService;

    @Autowired
    private AuthEntryPointJwt unauthorizedHandler;

    @Bean
    public AuthTokenFilter authenticationJwtTokenFilter() {
        return new AuthTokenFilter();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        authenticationManagerBuilder.userDetailsService(libraryUserDetailsService).passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable()
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .authorizeRequests().antMatchers("/register", "/token", "/h2-console/**").permitAll()
                .anyRequest().authenticated();
        http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    JwtDecoder jwtDecoder() {
        return NimbusJwtDecoder.withPublicKey(this.key).build();
    }

    @Bean
    JwtEncoder jwtEncoder() {
        JWK jwk = new RSAKey.Builder(this.key).privateKey(this.priv).build();
        JWKSource<SecurityContext> jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
        return new NimbusJwtEncoder(jwks);
    }
}
</code>


===== Utilidades para trabajar con JWT: JwtUtils =====

Contiene los métodos de utilidad **generateJwtToken** para generar un token (para los casos en los que se solicite) y **validateJwtToken** para verificar uno existente (para comprobar el que se indique en cualquier operación).

Se apoya en los Beans **JwtEncoder** y **JwtDecoder** que hemos definido en **LibraryConfig.java**

<code java>
@Component
@Configuration
public class JwtUtils {
    private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class);

    @Autowired
    private JwtEncoder jwtEncoder;

    @Autowired
    private JwtDecoder jwtDecoder;

    public String generateJwtToken(Authentication authentication) {
        Instant now = Instant.now();

        String scope = authentication.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.joining(" "));

        JwtClaimsSet claims = JwtClaimsSet.builder()
                .issuer("self")
                .issuedAt(now)
                .expiresAt(now.plus(1, ChronoUnit.HOURS))
                .subject(authentication.getName())
                .claim("scope", scope)
                .build();
        return jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
    }

    public String getUserNameFromJwtToken(String token) {
        return jwtDecoder.decode(token).getSubject();
    }

    public boolean validateJwtToken(String authToken) {
        try {
            Jwt jwt = jwtDecoder.decode(authToken);
            return true;
        } catch (SignatureException e) {
            logger.error("Invalid JWT signature: {}", e.getMessage());
        } catch (MalformedJwtException e) {
            logger.error("Invalid JWT token: {}", e.getMessage());
        } catch (ExpiredJwtException e) {
            logger.error("JWT token is expired: {}", e.getMessage());
        } catch (UnsupportedJwtException e) {
            logger.error("JWT token is unsupported: {}", e.getMessage());
        } catch (IllegalArgumentException e) {
            logger.error("JWT claims string is empty: {}", e.getMessage());
        }

        return false;
    }
}
</code>

===== Esquema de respuesta JWT: JwtResponse =====

Aqui definimos el formato que queremos para la respuesta a la petición de token. En este caso se devuelve el propio token (JWT), el nombre de usuario de quien lo solicita y los roles que tiene asignados (por ahora ninguno)

<code java>
@Data
@AllArgsConstructor
@NoArgsConstructor
public class JwtResponse {

    private String token;
    private String username;
    private List<String> roles;
}
</code>

===== Gestión de errores de autenticación: AuthEntryPointJwt =====

En esta clase, en el método **commence** se define cómo se quieren tratar los errores de autenticación. En este caso se registra una traza en en log y se responde con una respuesta vacía y el código de estado 401 Unauthorized.

<code java>
@Component
public class AuthEntryPointJwt implements AuthenticationEntryPoint {

    private static final Logger logger = LoggerFactory.getLogger(AuthEntryPointJwt.class);

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
                         AuthenticationException authException) throws IOException, ServletException {
        logger.error("Unauthorized error: {}", authException.getMessage());
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Error: Unauthorized");
    }
}
</code>

===== Procesamiento del JWT: AuthTokenFilter =====

<code java>
public class AuthTokenFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtils jwtUtils;

    @Autowired
    private LibraryUserDetailsService userDetailsService;

    private static final Logger logger = LoggerFactory.getLogger(AuthTokenFilter.class);

    @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);

                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        } catch (Exception exception) {
            logger.error("Cannot set user authentication: {}", exception);
        }

        filterChain.doFilter(request, response);
    }

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

        if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) {
            return headerAuth.substring(7);
        }

        return null;
    }
}
</code>

===== Gestión de credenciales de usuario: UserDetailsService =====

<code java>
@Service
public class LibraryUserDetailsService implements UserDetailsService {

    @Autowired
    private UserService userService;

    @Override
    @Transactional
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userService.findByUsername(username);
        if (user == null)
            throw new UsernameNotFoundException("Invalid username/password");

        List<GrantedAuthority> authorities = getUserAuthority(user.getRoles());
        return buildUserForAuthentication(user, authorities);
    }

    private List<GrantedAuthority> getUserAuthority(Set<Role> userRoles) {
        Set<GrantedAuthority> roles = new HashSet<>();
        userRoles.forEach(role -> roles.add(new SimpleGrantedAuthority(role.getName())));
        return new ArrayList<>(roles);
    }

    private UserDetails buildUserForAuthentication(User user, List<GrantedAuthority> authorities) {
        return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(),
                user.isActive(), true, true, true, authorities);
    }
}
</code>



===== Fichero de configuración: application.properties =====

<code bash>
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.globally_quoted_identifiers=true

server.port=8080

spring.datasource.url=jdbc:h2:file:/Users/santi/library_sec.db
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.h2.console.enabled=true

key.private=classpath:app.key
key.public=classpath:app.pub
</code>

===== Controllers: Registro y de usuarios y solicitud de token =====

==== Registro de usuarios ====

<code java>
@RequestMapping(value = "/register", method = RequestMethod.POST)
public ResponseEntity<?> saveUser(@RequestBody UserDTO user) throws Exception {
    return ResponseEntity.ok(userService.addUser(user));
}
</code>

==== Endpoint para solicitar token ====

<code java>
@PostMapping("/token")
public ResponseEntity<?> authenticateUser(@Valid @RequestBody UserDTO userDTO) {
    Authentication authentication = authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(userDTO.getUsername(), userDTO.getPassword()));

    SecurityContextHolder.getContext().setAuthentication(authentication);
    String jwt = jwtUtils.generateJwtToken(authentication);

    User userDetails = (User) authentication.getPrincipal();
    List<String> roles = userDetails.getAuthorities().stream()
            .map(GrantedAuthority::getAuthority)
            .collect(Collectors.toList());

    return ResponseEntity.ok(new JwtResponse(jwt,
            userDetails.getUsername(),
            roles));
}
</code>
----

===== Funcionamiento =====

  * [[https://datos.codeandcoke.com/apuntes:jwt#registro_de_usuarios|Registro de usuario]]
  * [[https://datos.codeandcoke.com/apuntes:jwt#solicitud_de_token|Solicitud de token]]
  * [[https://datos.codeandcoke.com/apuntes:jwt#ejecutar_operaciones_con_autenticacion|Ejemplo de operación con autenticación: Bearer token]]
  * [[https://datos.codeandcoke.com/apuntes:jwt#ejecutar_operaciones_sin_autenticacion|Ejemplo de operación no autenticada: Error 401 Unauthorized]]

==== Registro de usuarios ====

<figure>
{{ register.png }}
<caption>Registro de usuarios en la API</caption></figure>



==== Solicitud de token ====

<figure>
{{ token.png }}
<caption>Solicitud de token</caption></figure>
==== Ejecutar operaciones con autenticación ====

<figure>
{{ get_books.png }}
<caption>Petición con cabecera Authorization y Bearer token</caption></figure>

==== Ejecutar operaciones sin autenticación ====

<figure>
{{ 401.png }}
<caption>Error 401 al no especificar el token en la cabecera Authorization</caption></figure>
===== Ejemplo =====

Existe un ejemplo de API securizada con JWT tal y como se explica en este apartado. Está en el repositorio [[https://github.com/codeandcoke/spring-web|spring-web]] bajo el nombre [[https://github.com/codeandcoke/spring-web/tree/master/library-api-sec|library-api-sec]]

----

(c) 2022-{{date>%Y}} Santiago Faci
