Si tuviera que implementar un patrón de conexión OpenID común en un SPA, podría tener la siguiente relación:
Auth server <-----------> Client (browser) <-----------> App API server
El usuario sería redirigido al servidor de autenticación para iniciar sesión, y se configurará una cookie solo de HTTP en el servidor de autenticación con el token de ID del usuario (cuya carga útil contiene detalles del usuario y está firmado por un secreto) y el token de autenticación. Luego, el servidor de autenticación redirige al usuario a la aplicación con los tokens en la cadena de hash, para que puedan persistir.
El hash debería establecerse haciendo que la API devuelva algo como:
res.send('<script>
location.replace('${callbackUrl}#access_token=${accessToken}&id_token=${idToken}')
</script>')
en lugar de un redireccionamiento HTTP con el token en la cadena de consulta (porque probablemente se registrará), y la aplicación del lado del cliente puede usar history.replaceState()
para borrar los tokens del historial del navegador. El navegador quita estos tokens del hash para que el usuario no marque ni comparta accidentalmente esta URL que contiene los tokens, y luego los tokens persisten en localStorage
.
Recolecté de este artículo sobre Stormpath ese localStorage
no es la mejor idea, ya que es vulnerable a XSS (cualquier JS ejecutado arbitrariamente en la misma página puede leer el token de localStorage
).
Por lo tanto, abogan por el uso de cookies de solo HTTP para almacenar tokens en su lugar. En este enfoque, la API de autenticación redirige al usuario a la aplicación con encabezados para establecer cookies de solo HTTP con:
-
access_token
-
id_token
-
csrf_token
Estas cookies se configuran en el servidor de API de autenticación. La interfaz de usuario también debería saber el valor de csrf_token
. Si la autenticación se realizó con una solicitud AJAX, podríamos enviarla en un encabezado; en el caso de OpenID, donde la aplicación redirigió al dominio API, tendríamos que usar el enfoque script
y enviarlo en el hash para que la UI de la aplicación lo reciba. Según tengo entendido, estos enfoques son aproximadamente equivalentes en términos de seguridad, ya sea que se lean desde el hash o desde un encabezado, de cualquier manera el navegador debe almacenar este token CSRF para enviarlo en solicitudes posteriores . / p>
El servicio HTTP de Angular maneja el enfoque anterior (encabezado) al agarrar el token y guardarlo en una cookie en el dominio de la interfaz de usuario. Luego envía automáticamente las solicitudes a la API de autenticación con el X-XSRF-Token
establecido en ese valor que ha almacenado en la cookie. Luego, la API puede confirmar que este token dado coincide con el de su cookie csrf_token
y determinar si el usuario envió esta solicitud.
Esto es considerablemente más complicado que el enfoque localStorage
. El artículo propone que esto es más seguro porque localStorage
es vulnerable a XSS: un atacante que incrusta JS malicioso en una página en el dominio de la interfaz de usuario puede robar los valores de localStorage
. (Obviamente, XSS se puede mitigar, pero ciertamente existen situaciones en las que podemos ser vulnerables a XSS, como un CDN que la interfaz de usuario utiliza para piratear y enviar scripts maliciosos, o simplemente un paquete npm que parece benigno y se usa en la interfaz de usuario). pero en secreto captura localStorage
en el fondo.)
¿Pero cómo es más seguro este enfoque CSRF? Después de todo, el token CSRF debe almacenarse en una cookie en la interfaz de usuario, y no puede ser una cookie solo de HTTP, ya que las solicitudes a la API deben incluir el token en un encabezado. Entonces, ¿esto no significaría la misma vulnerabilidad XSS? El script malicioso podría leer la cookie CSRF en la interfaz de usuario y comenzar a enviar solicitudes a la API usándola.
¿Me estoy perdiendo algo aquí? ¿Cómo es más segura la cookie CSRF que poner un token en localStorage
, si en cualquier caso, el medio principal de autenticación con la API de autenticación puede leerse desde el cliente?