Simplificar la autenticación con OAuth en el edge
La autenticación intimida, parece algo difícil y peligroso... pero es esencial. La mayoría de las aplicaciones la necesitan, por lo que es una condición previa en casi todas las peticiones de los usuarios finales. La autenticación de aplicaciones web perfecta estaría cerca del usuario final, estaría aislada del resto del sistema y sería fácil de integrar. Además, su implementación y mantenimiento irían a cargo de profesionales de la seguridad.
Esta descripción nos recuerda a la OAuth implementada en Compute@Edge, en Fastly: rápida, descentralizada, segura y autónoma. Veamos cómo se implementa.
Empecemos por lo básico: la autenticación consiste en demostrar quién soy, lo cual difiere de la autorización, que es lo que tengo permiso para hacer. Primero nos centraremos en cómo confirmar la identidad del usuario. La forma más común de hacerlo es utilizando OAuth 2.0 y OpenID Connect.
Cuando llegan peticiones al borde, necesitamos poder separar las que se pueden vincular con un usuario particular de las que son anónimas o no válidas. Las peticiones anónimas se canalizan a través de un flujo de código de autorización de OAuth, mientras que las no válidas se rechazan. En consecuencia, solo pasan al servidor de origen de la aplicación las peticiones procedentes de usuarios autenticados.
Este es el flujo que crearemos, más detallado:
Repasemos:
El usuario solicita un recurso protegido, pero no tiene cookie de sesión.
En el edge, Fastly genera:
un parámetro de estado único e imposible de adivinar, que codifica lo que el usuario estaba tratando de hacer (cargar
/articles/kittens
);una cadena criptográfica aleatoria llamada verificador de código;
un desafío de código, derivado del verificador de código;
un token de tiempo limitado, autenticado utilizando un secreto, que codifica el estado y un nonce (un valor único utilizado para mitigar los ataques de repetición).
Almacenamos a) y b) en cookies para poder recuperarlos más adelante. Incluimos c) y d) en la siguiente petición destinada al servidor de autorización.
Fastly crea una URL de autorización y redirige al usuario al servidor de autorización operado por el proveedor de identidad.
El usuario realiza los trámites de inicio de sesión directamente con el proveedor de identidad (IdP). Fastly no participa en el proceso hasta que recibe una petición a una URL de devolución de llamada con los resultados del proceso de inicio de sesión. El IdP incluirá un código de autorización y un estado (que debería coincidir con el token de tiempo limitado que creamos anteriormente) en esa devolución de llamada posterior al inicio de sesión.
El servicio del edge autentica el token de estado devuelto por el IdP y verifica que el estado que almacenamos coincida con su declaración de sujeto.
Fastly se conecta directamente con el IdP e intercambia el código de autorización (que es válido para un solo uso) y el verificador de código para tokens de seguridad:
un token de acceso (una clave que representa la autorización para realizar ciertas operaciones en nombre del usuario);
un token de identificación, que contiene la información del perfil del usuario.
Fastly redirige al usuario final a la URL de la petición original (
/articles/kittens
), junto con sus tokens de seguridad almacenados en cookies.Cuando el usuario efectúa la petición redirigida (o cualquier petición posterior acompañada de tokens de seguridad), Fastly verifica la integridad, la validez y las declaraciones de ambos tokens. Si los tokens siguen siendo válidos, redirigimos la petición a tu origen mediante proxy.
Antes de empezar a diseñar todo lo anterior, necesitas un IdP.
Obtención de un proveedor de identidad (IdP)
Aunque puedes utilizar tu propio servicio de identidad, también te podría servir cualquier proveedor que se ajuste a OAuth 2.0 y OpenID Connect (OIDC). Sigue estos pasos para configurar el IdP:
Registra tu aplicación con el IdP. Apunta el
client_id
y la dirección del servidor de autorización.Guarda una copia local de los metadatos de detección de OpenID Connect del servidor. Los encontrarás en
/.well-known/openid-configuration
, en el dominio del servidor de autorización. Por ejemplo, estos son los que proporciona Google.Guarda una copia local de los metadatos del conjunto de claves web de JSON (JWKS). Lo encontrarás en la propiedad
jwks_uri
, en el documento de metadatos de detección que acabas de descargar.
Ahora, crea un servicio Compute@Edge en Fastly que se comunique con el IdP.
Creación del servicio Compute@Edge
Hemos puesto todo lo que hace falta para este proyecto en un repositorio de GitHub. Aun así, deberás tener una cuenta de Fastly que esté habilitada para los servicios de Compute@Edge. Si no lo has hecho todavía, sigue los pasos de la guía de bienvenida de Compute@Edge, que instalará la CLI de Fastly y la cadena de herramientas de Rust en tu equipo local. ¡Ahora podemos empezar a trabajar con código!
Clona este repositorio:
git clone https://github.com/fastly/compute-rust-auth
undefinedSigue las instrucciones del archivo README.
¡Enhorabuena, has desplegado un servicio de Compute@Edge! Para completar la integración, tu IdP necesita saber que debe dirigir a los usuarios al servicio de Compute@Edge una vez hayan iniciado sesión.
Enlace con el IdP
Añade https://{some-funky-words}.edgecompute.app/callback
a la lista de URL de devolución de llamada permitidas en la configuración de la aplicación de tu IdP. Así, el servidor de autorización podrá enviar al usuario de vuelta a tu sitio web, que está alojado en Fastly.
Abre tu aplicación en un navegador para ver el fruto de tu trabajo:
A efectos de la aplicación de ejemplo, cualquier ruta que intentes visitar requerirá autenticación. Si ya estás autenticado, Fastly enviará la petición a tu origen mediante proxy, como es habitual. Con esto puedes enfrascarte en tareas más ambiciosas, pero hablaremos de ello pronto en otro artículo. De momento, veamos cómo funciona esta integración básica y mínima.
Cómo se integra todo en Compute@Edge
Los programas de Compute@Edge escritos en Rust que utilizan el SDK de Rust de Fastly tienen una función main
que es el punto de entrada de las peticiones. Normalmente, la función main
recibe una estructura Request y devuelve una estructura Response.
Lo primero que hacemos en main
es comprobar si se trata de una ruta de devolución de llamadas. En caso afirmativo, indicaría que el usuario vuelve del IdP y está listo para inicializar una sesión. Esta ruta siempre debe ser interceptada y nunca será redirigida al backend mediante proxy.
if req.get_url_str().starts_with(&redirect_uri) {
// ... snip: Validate state and code_verifier ...
// ... snip: Check authorization code with identity provider ...
// ... snip: Identity provider returns access & ID tokens ...
Ok(responses::temporary_redirect(
original_req,
cookies::session("access_token", &auth.access_token),
cookies::session("id_token", &auth.id_token),
cookies::expired("code_verifier"),
cookies::expired("state"),
))
}
Una respuesta satisfactoria del IdP consumirá el código de autorización y la clave de prueba (¿te acuerdas del verificador de código y el desafío?) y devolverá un access_token
e id_token
:
El token de acceso es un token de portador. Esto significa que el «portador» está autorizado a actuar en nombre del usuario para acceder a los recursos autorizados.
Un token de identificación es un token web de JSON (JWT) que codifica información de identidad sobre el usuario.
Los utilizaremos para autenticar peticiones futuras, así que los almacenaremos en cookies, eliminaremos toda cookie intermedia que hayamos utilizado para la autenticación (¿te acuerdas del parámetro de estado y el verificador de código?) y redirigiremos al usuario de vuelta a la URL que quería al principio.
Si sabemos que el usuario no está en la ruta de devolución de llamada, entonces revisamos las cookies que acompañan a la petición para determinar si hay una sesión activa (definida por el token de acceso y el token de identificación):
let cookie = cookies::parse(req.get_header_str("cookie").unwrap_or(""));
if let (Some(access_token), Some(id_token)) = (cookie.get("access_token"), cookie.get("id_token")) {
// snip: ... validation logic ...
req.set_header("access-token", access_token);
req.set_header("id-token", id_token);
return Ok(req.send("backend")?);
}
Si la sesión existe y es válida, req.send()
envía la petición en sentido ascendente hacia tu propio origen, y devuelve una Response que debe enviarse en sentido descendente al cliente. Nuestro servicio de ejemplo establece el token de acceso y el token de identificación como encabezados HTTP personalizados dirigidos al origen. En este caso, puedes hacer algo distinto según cómo tengas pensado utilizar la identidad en el origen. En nada lo vemos.
Por último, si el usuario no está autenticado ni está iniciando sesión, empezamos nosotros el proceso enviándolo al IdP para que inicie sesión:
let authorize_req =
Request::get(settings.openid_configuration.authorization_endpoint)
.with_query(&AuthCodePayload {
client_id: &settings.config.client_id,
code_challenge: &pkce.code_challenge,
code_challenge_method: &settings.config.code_challenge_method,
redirect_uri: &redirect_uri,
response_type: "code",
scope: &settings.config.scope,
state: &state_and_nonce,
nonce: &nonce,
})
.unwrap();
Ok(responses::temporary_redirect(
authorize_req.get_url_str(),
cookies::expired("access_token"),
cookies::expired("id_token"),
cookies::session("code_verifier", &pkce.code_verifier),
cookies::session("state", &state),
))
En este ejemplo, utilizamos Request::get para crear una petición. Pero en lugar de enviarla, extraemos la URL serializada y redirigimos al usuario a la misma. También guardamos el parámetro de estado y el verificador de código para poder comprobarlos más tarde. Y eliminamos los tokens de acceso e identificación, porque, si los hay, es evidente que nuestro código anterior no los aprobó.
Estas son las tres situaciones en las que nuestra solución de autenticación interactúa con las peticiones entrantes.
Conclusión
El concepto básico de realizar la autenticación y luego utilizar los datos de identidad para tomar decisiones de autorización es algo que se aplica a la mayoría de aplicaciones web y nativas. Al hacerlo en el edge, podemos ofrecer grandes ventajas tanto a desarrolladores como a usuarios finales:
Mayor seguridad, ya que el proceso de autenticación consiste en una única implementación universal que se aplica a todas las aplicaciones del backend.
Mantenimiento más fácil, porque los componentes están desacoplados.
Respeto por la privacidad, porque mantenemos los datos del usuario donde deben estar y compartimos la mínima cantidad de información con las aplicaciones del origen.
Rendimiento más rápido, porque en el edge podemos almacenar en caché el contenido que requiere autenticación, y las peticiones derivadas del proceso de autenticación se responden en el mismo edge.
Este es solo uno de los muchos casos de uso posibles de la informática en el edge. También sirve de ejemplo sobre cómo desvincular el estado de la infraestructura central. Gran parte del problema de hacer crecer sistemas descentralizados radica en lograr que el estado sea más efímero, o más asíncrono. Andrew Betts compartió hace poco en otro artículo más ideas acerca de las aplicaciones nativas del edge.
¡Tenemos muchas ganas de ver qué otros usos hacéis de Compute@Edge!