1 of 96

Seguridad y Autenticación en aplicaciones web

Programación con Herramientas Modernas

2 of 96

Antes de arrancar

Verificá que tengas activada la opción “Disable cache” en el navegador (F12 > solapa Network)

3 of 96

Conceptos básicos de seguridad

Vamos a conocer algunos conceptos antes de meternos en el módulo de seguridad que propone Springboot.

4 of 96

Autenticación

La autenticación es el proceso de identificación de un usuario o proceso para acceder a un sistema.

5 of 96

Autenticación

Algunas formas de autenticación

datos biométricos

con tokens

usuario/password

con API Keys

6 of 96

Autorización

La autorización consiste en entender a qué recursos tiene acceso un usuario. Por ejemplo, un docente puede cargar notas de todos sus alumnes, pero une alumne solo puede ver sus notas.

7 of 96

Vulnerabilidades

Las vulnerabilidades son debilidades existentes en un sistema que pueden ser comprometer la seguridad de la información de una empresa. Parte del daño que pueden causar van desde la filtración o robo de datos, caída de la aplicación, pérdida de la confianza de los clientes, etc.

8 of 96

OWASP y el top 10 de vulnerabilidades

Existe un proyecto open-source llamado OWASP (Open Web Application Security Project), que se dedica a analizar y combatir las causas de que un software sea inseguro. Cada cierta cantidad de años OWASP lanza un reporte del top 10 de vulnerabilidades (en breve saldrá el reporte del 2025):

9 of 96

Un ejemplo: Cross Site Request Forgery (CSRF)

Supongamos que interactuamos con una app. En el login pedimos el formulario HTTP para ingresar nuestras credenciales.

GET /login

HTML 200 Form

10 of 96

CSRF

  • En este caso el cliente envía usuario y contraseña al endpoint auth.
  • El servidor valida las credenciales y devuelve un código interno de la sesión (sessionId)...
  • ...que se almacena dentro del cliente en un archivo del navegador llamado cookie.

Eso permite que la próxima llamada nosotros pasemos esa cookie con la cual el servidor sabe que se trata de una sesión válida.

POST /auth

{ usuario: ‘juan’, password: ‘cito’ }

sessionId: #hash

11 of 96

CSRF

Por ejemplo, en una aplicación bancaria podríamos tener un formulario para transferir dinero a una cuenta mediante un request POST con la información accountNumber y amount. Junto con esos valores, nosotros transferimos también la cookie con el sessionId.

POST /transfer

{ accountNumber: 123, amount: 100 }

200 OK

El sessionId corresponde a un usuario válido

12 of 96

CSRF

Si activás las Herramientas del Navegador, vas a poder ver las cookies en la solapa Application (en Chrome) o Storage (en Firefox).

13 of 96

CSRF

Ahora bien, qué pasa si recibimos un mail del tipo “Su cuenta será bloqueada próximamente” con un link.

Mail con un link

La cookie sigue activa porque nos logueamos previamente

14 of 96

CSRF

Ahora bien, qué pasa si recibimos un mail del tipo “Su cuenta será bloqueada próximamente” con un link.

Mail con un link

Hacemos click en el link, que nos lleva al sitio del hacker

15 of 96

CSRF

Ahora bien, qué pasa si recibimos un mail del tipo “Su cuenta será bloqueada próximamente” con un link.

Mail con un link

200 OK, con un formulario que tiene campos ocultos (o directamente código JS)

16 of 96

CSRF

Ahora bien, qué pasa si recibimos un mail del tipo “Su cuenta será bloqueada próximamente” con un link.

Mail con un link

Banco Alas

Reactivar cuenta

17 of 96

CSRF

Al presionar en el botón “reactivar cuenta” lo que hacemos en el cliente es llamar a la app de Home Banking transfiriendo el account number del hacker. En cada request http las cookies se transfieren automáticamente, lo cual en este caso es malo.

Mail con un link

Reactivar cuenta

POST /transfer

{ accountNumber: 9999, amount: 100 }

9999 es la cuenta del hacker

18 of 96

CSRF

Por eso las cookies expiran, pero mientras esté activa hay altas chances de que el atacante tenga éxito.

200 OK

19 of 96

CSRF

Te dejamos dos videos que explican en español los conceptos detrás de CSRF.

20 of 96

CSRF > Synchronizer Token Pattern

Existen varios mecanismos para evitar Cross Site Request Forgery. El primero que veremos es el Synchronizer Token Pattern.

21 of 96

CSRF > Synchronizer Token Pattern

Con cada request el servidor genera un CSRF Token, otro hash que viaja al cliente y que puede renovarse con cada request1. La diferencia es que no la guardamos en una cookie, sino que la recibimos y la asociamos a cualquier formulario en un input hidden.

POST /auth

{ usuario: ‘juan’, password: ‘cito’ }

sessionId: #hash

csrf token: #otroHash

1 Eso de yapa evita submitir dos veces un mismo form (evita así que compres 2 veces)

22 of 96

CSRF > Synchronizer Token Pattern

Al definir <input type="hidden" name="_csrf" value="..."> cuando hacemos SUBMIT en el formulario enviamos ese token. Para eso es importante además respetar la convención REST: los métodos http GET, OPTIONS, HEAD no deben producir efecto (como transferir de una cuenta a otra).

POST /transfer

{ accountNumber: 123, amount: 100,

_csrf: #otroHash }

200 OK

23 of 96

CSRF > Synchronizer Token Pattern

Qué pasa si recibimos un mail del tipo “Su cuenta será bloqueada próximamente” con un link: el hacker puede pasarnos un formulario con sus datos...

Mail con un link

La cookie sigue activa porque nos logueamos previamente, pero no tenemos el _csrf

24 of 96

CSRF > Synchronizer Token Pattern

Al hacer click en “Reactivar cuenta” se hace submit sobre el formulario, pero no tiene el valor del _csrf porque es propio de cada usuario: el hacker solo puede obtener un _csrf si él hace una petición con su usuario al banco (cosa que no le sirve para lograr que otra persona le transfiera a su cuenta)

Mail con un link

Reactivar cuenta

POST /transfer

{ accountNumber: 9999, amount: 100 }

25 of 96

CSRF > Synchronizer Token Pattern

Si no se envía el _csrf, al enviar el POST el server verifica que el token sea válido y lo rechaza.

Mail con un link

401 Unauthorized

26 of 96

CSRF > Same site attribute

Otra alternativa para evitar el Cross Site Request Forgery (CSRF) es utilizar la estrategia Same site attribute (definido en RFC6265bis).

El server envía con la respuesta una configuración para la cookie llamada SameSite

GET /login

HTML 200 Form

Set-Cookie: JSESSIONID=randomid; Domain=bank.example.com; Secure; HttpOnly; SameSite=<value>

servidor A

27 of 96

CSRF > Same site attribute

¿Cuando dos URLs comparten el mismo same site?

Cuando coinciden en el protocolo (http / https)

Cuando tienen el mismo dominio (unsam.edu.ar, youtube.com, etc.)

No entra en juego aquí el puerto, ni el subdominio (www)

servidor A

servidor B

28 of 96

CSRF > Same site attribute Strict

Las configuraciones posibles son: strict, en ese caso ninguna petición a otro servidor envía la cookie del servidor A.

HTML 200 Form

Set-Cookie: JSESSIONID=randomid; Domain=phm.edu.ar; Secure; HttpOnly; SameSite=Strict

servidor A

http://phm.edu.ar

servidor B

http://imagenesCopadas.com/

(necesita autenticación)

no se envía la cookie al servidor B si el dominio no coincide con el servidor A

GET http://imagenesCopadas.com/dodain.png

29 of 96

CSRF > Same site attribute Lax

Otra opción es lax, en ese caso mandaremos la cookie si cambiamos la navegación top-level (desde el navegador, haciendo un redirect) o bien si utilizamos un método HTTP seguro (GET, HEAD pero no POST)

HTML 200 Form

Set-Cookie: JSESSIONID=randomid; Domain=phm.edu.ar; Secure; HttpOnly; SameSite=Lax

servidor A

http://phm.edu.ar

no se envía la cookie porque el dominio del servidor B no coincide con el servidor A y no es una navegación

img src= "http://imagenesCopadas.com/dodain.png"

servidor B

http://imagenesCopadas.com/

(necesita autenticación)

30 of 96

CSRF > Same site attribute Lax

Otra opción es lax, en ese caso mandaremos la cookie si cambiamos la navegación top-level (desde el navegador, haciendo un redirect) o bien si utilizamos un método HTTP seguro (GET, HEAD pero no POST)

HTML 200 Form

Set-Cookie: JSESSIONID=randomid; Domain=phm.edu.ar; Secure; HttpOnly; SameSite=Lax

servidor A

http://phm.edu.ar

se envía la cookie si nosotros ponemos un link que redirige la url del navegador y es un método GET (es top level navigation)

servidor B

http://imagenesCopadas.com/

(necesita autenticación)

31 of 96

CSRF > Same site attribute Lax

Si no definís en tu server una configuración para Same Site attribute, por defecto los navegadores utilizan la variante Lax.

La excepción es Safari que permite que lo configures vos.

32 of 96

CSRF > Same site attribute None

La última configuración es None, que básicamente envía siempre las cookies, no importa si los sites son iguales o no, o el método http que utilizamos para acceder al recurso.

HTML 200 Form

Set-Cookie: JSESSIONID=randomid; Domain=phm.edu.ar; Secure; HttpOnly; SameSite=None

servidor A

http://phm.edu.ar

siempre enviamos la cookie

cualquier operación

servidor B

http://imagenesCopadas.com/

(necesita autenticación)

33 of 96

CSRF > Same site attribute

Te dejamos un video que si bien está en inglés es muy claro explicando cómo funcionan cada una de las configuraciones de Same Site

  • strict
  • lax
  • none
  • y el default para Chrome desde la versión 80

34 of 96

XSS: Cross Site Scripting

El cross site scripting es parecido al CSRF pero en este caso

  • involucra código Javascript en el navegador
  • y el daño que puede causar es mayor, porque no solo intercepta los requests sino también las responses

35 of 96

XSS: Cross Site Scripting

¿Pero qué tan frecuente es?

En 2017 era la séptima causa de vulnerabilidad.

El tweet que está a la derecha generó un ataque XSS para retweetearse cada vez que se mostraba.

el tweet que generó el exploit

36 of 96

Stored XSS

  • Stored XSS: almacenamos valores en la base sin tomar en cuenta que pueden contener código malicioso (el problema está en confiar en que la variable messageContent no tendrá código ejecutable javascript). Link al ejemplo.

function handleMessageSend(messageId, senderEmail, messageContent) {

database.save(messageId, senderEmail, messageContent)

}

function generateMessageHTML(messageId) {

const messageContent = database.loadContent(messageId);

return `<p class="messageContent">${messageContent}</p>`

}

<script>alert(’Hackeado!’)</script>

37 of 96

Reflected XSS

  • Reflected XSS: el código malicioso no se guarda sino que es utilizado inmediatamente para que el servidor “refleje” información del usuario logueado. Link al ejemplo.

app.get("/results", (req, res) => {

const searchQuery = req.query.search;

const results = getResults(searchQuery); // Implementation not shown

res.send(`

<h1>You searched for ${searchQuery}</h1>

<p>Here are the results: ${results}</p>`);

});

Código en el server - Express

38 of 96

DOM-based XSS

  • DOM-based XSS: el código malicioso manipula el DOM para agregar código que permita robar información. Link al ejemplo.

39 of 96

Cómo mitigar XSS

  • Validar todos los inputs que recibimos: en especial los campos que son de texto libre. Una técnica que podemos hacer es convertir los caracteres <, >, & en equivalentes html: &lt;,&gt;, &amp;, etc. lo que se conoce como “escapear”. Podés ver la recomendación de OWASP en esta página.

40 of 96

Cómo mitigar XSS

  • Si hay páginas que no deben contener Javascript, usar el atributo Content Type adecuado. Por ejemplo, si el servidor del backend responde a un endpoint con un JSON, asignarle

Content-Type: application/json;charset=UTF-8

permite que el navegador desactive Javascript en el cliente.

{"name":"<script>alert(1)</script>","title":"bar"}

Recibir como respuesta este JSON en nuestra app cliente no ejecutará el alert en el navegador si definimos que el content type es JSON.

41 of 96

Cómo mitigar XSS

La última medida de defensa es activar el Content Security Policy (CSP). Podemos definir mediante el http header qué recursos podemos descargar en el cliente con seguridad. Veamos un ejemplo:

Aquí vemos que solo podemos descargar código js si el cliente y el server coinciden con el mismo sitio, y podemos descargar imágenes del mismo sitio o bien de www.example.com

42 of 96

Spring Security

Spring Security es la propuesta de Springboot para manejar la seguridad en lo que es

43 of 96

Spring Security

Spring Security es la propuesta de Springboot para manejar la seguridad en lo que es

  • autenticación: validar credenciales de usuario

44 of 96

Spring Security

Spring Security es la propuesta de Springboot para manejar la seguridad en lo que es

  • autorización: entender a qué recursos tiene acceso un usuario.

45 of 96

Spring Security

Spring Security es la propuesta de Springboot para manejar la seguridad en lo que es

  • protección contra ataques: como CSRF

46 of 96

Spring Security: Autenticación y Roles

Vamos a definir qué endpoints deben estar autenticados, y qué roles deben tener acceso a cada caso de uso

Usuarios admin: pueden ver y actualizar información

Usuarios comunes: solo pueden ver información

Usuarios sin autenticar: solo pueden intentar loguearse o ver una página con errores

47 of 96

Spring Security: Autenticación

Vamos a definir qué endpoints deben estar autenticados, y qué roles deben tener acceso a cada caso de uso

@Bean

fun securityFilterChain(httpSecurity: HttpSecurity): SecurityFilterChain {

return httpSecurity...

.authorizeHttpRequests {

it.requestMatchers("/login").permitAll()

it.requestMatchers("/error").permitAll()

it.requestMatchers(HttpMethod.OPTIONS).permitAll()

// Permisos de admin para modificar

it.requestMatchers(HttpMethod.POST, "/heladerias").hasAuthority("ADMIN")

it.requestMatchers(HttpMethod.POST, "/duenios").hasAuthority("ADMIN")

it.requestMatchers(HttpMethod.PUT, "/heladerias/**").hasAuthority("ADMIN")

// Default: que se autentique para poder ver información

.anyRequest().authenticated()

}

48 of 96

Spring Security: Autenticación

¿Dónde defino los usuarios y los roles?

Keycloak: provee un mecanismo de Single Sign On (SSO, o login unificado), federación de usuarios (compartir un usuario entre varios servers)

49 of 96

Spring Security: Autenticación

¿Dónde defino los usuarios y los roles?

OAuth2: te podés integrar con servicios de autenticación externos, como tu correo de Google, Facebook, Github, etc.

50 of 96

Spring Security: Autenticación

¿Dónde defino los usuarios y los roles?

InMemoryUserDetailsManager: genera una base de usuarios en memoria cada vez que se levanta el servidor

51 of 96

Spring Security: Autenticación

¿Dónde defino los usuarios y los roles?

O creamos nuestras tablas de Usuarios y Roles y los vamos a integrar con Spring Security.

52 of 96

Spring Security: Autenticación

De hecho... en lugar de "ADMIN" vamos a usar ROLES.ADMIN.name para trabajar los roles como wko (objetos bien conocidos), implementados como enums.

53 of 96

Spring Security: Autenticación

Un dato importante, la password se encripta en la base. No es 100% seguro pero es mejor que tenerlo libre...

class Usuario {

private var password: String = ""

private fun getDefaultEncoder(): PasswordEncoder =

Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8()!!

fun crearPassword(rawPassword: String) { password = getDefaultEncoder().encode(rawPassword) }

54 of 96

Spring Security: Autenticación

Una vez que validamos usuario y password, queremos que la comunicación entre cliente y servidor fluya. Para eso vamos a usar una firma digital: un token, que no es otra cosa que una serie de bytes.

55 of 96

Springboot: Autenticación con JWT

Presentamos a JWT: JSON Web Token, donde el token representa

  • información sobre la sesión asociada a un usuario identificado
  • con formato JSON
  • y firmado digitalmente, donde un algoritmo utiliza una clave como forma de encriptar los datos.

Podemos ver https://jwt.io/

56 of 96

Springboot: Autenticación con JWT

En la autenticación por JWT el usuario y la contraseña se validan y lo que vuelve es un token, que hay que utilizar en cada una de las llamadas siguientes (recordemos que http es stateless).

57 of 96

Springboot: Autenticación con JWT

Vemos la implementación en Springboot:

@Bean

fun securityFilterChain(httpSecurity: HttpSecurity): SecurityFilterChain {

return httpSecurity

...

.httpBasic(Customizer.withDefaults())

.sessionManagement { configurer ->

configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)

}

// definimos aquí que cada request tendrá JWT para identificar quién lo envía

.addFilterBefore(jwtAuthorizationFilter, UsernamePasswordAuthenticationFilter::class.java)

httpBasic permite capturar el header `Authorization` de cada request con el Bearer Token

Luego de analizar el login veremos qué hace este filter (decorator)

58 of 96

Springboot: Autenticación con JWT

El endpoint de login no tiene autenticación, delega la validación al service y devuelve el token:

Pueden ver cómo la implementación del service delega la búsqueda al repositorio y cierta lógica al dominio (como verificar la contraseña en base al algoritmo de encriptación)

@PostMapping("/login")

fun login(@RequestBody credencialesDTO: CredencialesDTO): String {

return usuarioService.login(credencialesDTO)

}

@Transactional(Transactional.TxType.NEVER)

fun login(credencialesDTO: CredencialesDTO): String {

val usuario = validarUsuario(credencialesDTO.usuario)

usuario.validarCredenciales(credencialesDTO.password)

return tokenUtils.createToken(credencialesDTO.usuario,

usuario.roles.map { it.name })!!

}

59 of 96

Springboot: Autenticación con JWT

El algoritmo que genera el token es

es importante especificar un vencimiento para el token (si te hackean es limitado el daño que puede causar

fun createToken(nombre: String, roles: List<String>): String? {

val longExpirationTime = accessTokenMinutes.minutes.inWholeMilliseconds

val now = Date()

return Jwts.builder()

.subject(nombre)

.issuedAt(now)

.expiration(Date(now.time + longExpirationTime))

.claim("roles", roles)

.signWith(Keys.hmacShaKeyFor(secretKey.toByteArray()))

.compact()

}

60 of 96

Springboot: Autenticación con JWT

Configuraciones como el vencimiento del token o el secret key se definen como autowired:

Eso se configura en el application.yml, o bien en un application-dev.yml que no subimos a git (al menos el secret):

@Component

class TokenUtils {

@Value("\${security.secret-key}")

lateinit var secretKey: String

@Value("\${security.access-token-minutes}")

var accessTokenMinutes: Int = 60

security:

secret-key: ...

access-token-minutes: 300

61 of 96

JWT: Login desde Bruno

El login nos devuelve un código http 401 si las credenciales no son válidas (“credenciales inválidas”, sin especificar demasiado) o el token correspondiente:

62 of 96

JWT: Login desde Bruno

Ese token se almacena como variable Post Response...

63 of 96

JWT: Login desde Bruno

...y se define como Header en otros endpoints:

64 of 96

JWT: Login desde POSTMAN

Configuramos un script post-response:

pm.environment.set("token", pm.response.text())

65 of 96

JWT: Login desde POSTMAN

...y se define como Bearer Token en otros endpoints:

66 of 96

JWT: Login desde Insomnia

Vemos cómo podemos configurar las llamadas a otros endpoints desde Insomnia tomando como input el token que recibimos en el login:

67 of 96

Springboot: Autenticación con JWT

El JWTAuthorizationFilter decora todos los request que reciben el header Authorization para procesar el token y asignarle el usuario/rol correspondiente...

override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) {

val bearerToken = request.getHeader("Authorization")

if (bearerToken != null && bearerToken.startsWith("Bearer ")) {

val token = bearerToken.substringAfter("Bearer ")

val usernamePAT = tokenUtils.getAuthentication(token)

usuarioService.validarUsuario(usernamePAT.name)

SecurityContextHolder.getContext().authentication = usernamePAT

68 of 96

Login: Qué pasa en el frontend

En el login vamos a recibir el token, lo almacenamos en el LocalStorage del navegador:

69 of 96

Login: Qué pasa en el frontend

Vemos el código que resuelve esta parte, en el service que maneja el usuario:

export async function loginUser(usuario: string, password: string) {

const token = await httpRequest<string>({

method: 'POST',

url: `${BACKEND_URL}/login`,

data: { usuario, password }

})

localStorage.setItem(TOKEN_KEY, token)

}

70 of 96

Login: Qué pasa en el frontend

Las alternativas para almacenar el token son: guardarlo en el Local Storage (eso persiste aun cuando cerramos el navegador), o bien en el Session Storage (está atado a la sesión, al cerrar el navegador se borra la información), o guardarlo en una cookie, técnica que usaremos más adelante para evitar CSRF. Podés profundizar en este link.

71 of 96

Login: salir de la aplicación

Tenemos un botón que borra el token manualmente:

72 of 96

Login: salir de la aplicación

El código es bastante sencillo, eliminamos el token y redirigimos al login:

const logout = () => {

localStorage.removeItem(TOKEN_KEY)

navigate({

to: '/login',

search: {

redirect: '/',

},

})

}

token

73 of 96

Login: sesión vencida (backend)

Cada vez que hacemos un pedido, el server valida que el token no haya expirado. Si el token se vence atrapamos la excepción para que el mecanismo de Springboot no emita un error http 500: por más que asociemos el código 401 a TokenExpiradoException, necesitamos explícitamente setear el error de la respuesta.

} catch (e: TokenExpiradoException) {

logger.warn(e.message)

response.sendError(HttpStatus.UNAUTHORIZED.value(), e.message)

}

74 of 96

Login: sesión vencida (frontend)

...ese 401 lo recibimos como error en el service. Dentro del mecanismo de routing nosotros definimos una ruta específica cuando ocurre un error:

export const Route = createRootRoute({

component: RootComponent, // el componente principal

errorComponent: RouteErrorComponent,

})

75 of 96

Login: sesión vencida (frontend)

Ese componente llama a un modal (también llamada ventana de diálogo, un Toast un poco más elaborado), que se activa como un contenedor por encima de la página actual para mostrar un mensaje de error:

const RouteErrorComponent = ({ error, reset }: ErrorComponentProps) => {

const router = useRouter()

const onRetry = async () => {

await router.invalidate()

reset()

}

return <ErrorModal error={error as AxiosError} title={'Ups! Hubo...'} onRetry={onRetry} />

}

export default RouteErrorComponent

76 of 96

Login: sesión vencida (frontend)

El modal muestra el error y redirige al login:

const errorMessage = sessionExpired

? 'La sesión ha expirado. Por favor, inicie sesión nuevamente.' : ...

return (

<Modal ... isOpened={!!error} close={onClose}>

<ModalResponseContent ... <p>{errorMessage}</p>

actions={[

sessionExpired && (

<Button label='Navegar a login'

key='goToLogin' type='button' onClick={logout} />

), ...

77 of 96

Login: sesión vencida (frontend)

Además, en cada ruta tenemos una función general para manejar errores. Allí preguntamos si venció la sesión y borramos el token para evitar mandarlo la próxima vez...

// errors.ts

export const isSessionExpired =

(error: AxiosError) => error.status === HttpStatusCodes.UNAUTHORIZED

// routes.ts

export const onErrorRoute = (error: AxiosError) => {

if (isSessionExpired(error)) {

localStorage.removeItem(TOKEN_KEY)

}

}

78 of 96

Login: roles (app)

Si intentamos utilizar el usuario phm para actualizar la heladería, vemos que no tenemos permisos suficientes:

79 of 96

Login: roles (backend)

Recordemos que al desencriptar el token podemos saber qué usuario somos, y por lo tanto qué rol:

// TokenUtils.kt

fun getAuthentication(token: String): UsernamePasswordAuthenticationToken {

try {

val secret = Keys.hmacShaKeyFor(secretKey.toByteArray())

val claims = Jwts.parser().verifyWith(secret).build()

.parseSignedClaims(token).payload

...

val roles = (claims["roles"] as List<*>).map { SimpleGrantedAuthority(it.toString()) }

return UsernamePasswordAuthenticationToken(claims.subject, null, roles)

80 of 96

Login: roles (backend)

Y también recordemos que en la configuración de Spring Security definimos qué roles pueden acceder a qué endpoints. El de la actualización de la heladería solo está permitido para ADMIN:

Lo bueno es que no necesitamos hacer nada en nuestro endpoint, la configuración intercepta la llamada y devuelve un código http 403 (forbidden)...

.authorizeHttpRequests {

...

it.requestMatchers(HttpMethod.PUT, "/heladerias/**").hasAuthority(ROLES.ADMIN.name)

81 of 96

Login: roles (frontend)

...ese 403 lo recibimos como error en el service y se activa nuevamente la ventana modal (es el mismo código que con el vencimiento del token pero con un mensaje diferente).

82 of 96

Spring Security - CORS

Un cliente puede pedir al servidor imágenes, css, código javascript, videos, entre otras cosas

Cliente Web

Servidor Web

83 of 96

Spring Security - CORS

Pero para hacer pedidos mediante fetch o axios, utilizamos de fondo el objeto XMLHttpRequest, que requiere un mecanismo de negociación entre cliente y servidor, donde el servidor nos tiene que indicar qué orígenes están autorizados a hacer pedidos (requests). Ése es básicamente el mecanismo CORS

Cliente Web

Servidor Web

XMLHttpRequest

fetch/axios

84 of 96

Spring Security - CORS

La mayoría de los navegadores hacen una solicitud preflight a través de un método http OPTIONS para saber si es un origen válido para hacer el pedido correspondiente (que será GET, POST, etc.) ...

Cliente Web

Servidor Web

preflight / OPTIONS

85 of 96

Spring Security - CORS

el server le responde con headers sobre los orígenes válidos (p. ej: “https://elfront.com” o “*” que indica que acepta cualquier origen) y métodos http aceptados (p. ej: podría aceptar POST o GET pero no PUT).

Cliente Web

Servidor Web

headers

86 of 96

Spring Security - CORS

En resumen, el preflight devuelve 200 (todo ok) o un código de error (no tenés permisos para hacer este pedido 403 o 204, No Content).

87 of 96

Spring Security - CORS

Y todas las veces que hacemos un request, los navegadores hacen la negociación:

88 of 96

Spring Security - CORS

Vemos que a cada método le precede una llamada a OPTIONS (siempre tener en cuenta la opción “Disable cache”)

89 of 96

Spring Security - CORS

Desactivamos aquí CORS...

@Bean

fun securityFilterChain(httpSecurity: HttpSecurity): SecurityFilterChain {

return httpSecurity

.cors { it.disable() }

90 of 96

Spring Security - CORS

...porque luego le activamos esta configuración:

@Bean

fun corsConfigurer(): WebMvcConfigurer {

return object : WebMvcConfigurer {

override fun addCorsMappings(registry: CorsRegistry) {

registry.addMapping("/**")

.allowedOrigins("http://localhost:5173")

.allowedHeaders("*")

.allowedMethods("POST", "GET", "PUT", "DELETE")

.allowCredentials(true)

}

}

}

queremos que el front sea reconocido (modo dev local)

y qué métodos http vamos a usar

91 of 96

CSRF: Backend

Una medida extra de seguridad que vamos a agregar es la protección contra CSRF, donde el server pasará como respuesta una cookie con un token: el token sirve únicamente para el usuario logueado (no sirve para hacer pedidos desde otro sitio).

@Bean

fun securityFilterChain(httpSecurity: HttpSecurity): SecurityFilterChain {

return httpSecurity

...

.csrf {

it.ignoringRequestMatchers("/login")

it.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())

it.csrfTokenRequestHandler(SpaCsrfTokenRequestHandler())

el login no requiere token

el token lo pasamos a través de una cookie, httpOnly = false para que el frontend nos la pueda mandar

92 of 96

CSRF: Backend

Para manipular el token XSRF vamos a configurarle un handler específico. La implementación podés verla en el repositorio, lo importante es que podemos recibir el token

  • a través del header
  • o bien por un input type hidden

Solo se chequea el token en operaciones con efecto (PUT, POST, etc.)

93 of 96

CSRF: frontend

Axios ya trae una configuración para recibir la cookie con la clave XSRF-TOKEN y enviar el header con la clave X-XSRF-TOKEN:

export async function httpRequest<T>(request: AxiosRequestConfig): Promise<T> {

...

const okRequest = {

...request,

withXSRFToken: true,

withCredentials: true,

xsrfHeaderName: 'X-XSRF-TOKEN',

xsrfCookieName: 'XSRF-TOKEN',

}

const response = await axios(okRequest)

return response.data

}

94 of 96

CSRF: interacción

Vemos cómo funciona el mecanismo:

95 of 96

Analizadores de vulnerabilidades

96 of 96

¡Gracias!