#352 Securing an API
- Download:
- source codeProject Files in Zip (95.7 KB)
- mp4Full Size H.264 Video (20 MB)
- m4vSmaller H.264 Video (9.66 MB)
- webmFull Size VP8 Video (7.81 MB)
- ogvFull Size Theora Video (25.9 MB)
La semana pasada en el episodio 350 vimos cómo construir una API versionada para una tienda. Si visitamos la ruta /api/products
podemos interactuar con la aplicación mediante JSON. La API es totalmente pública lo que quiere decir que cualquiera puede utilizarla y editar o borrar los registros. Normalmente querremos que esto no sea así y que el acceso esté restringido. Hay varias maneras de hacer esto, y la forma correcta depende de los requisitos de nuestra aplicación. En este episodio vamos a ver algunas soluciones que se pueden usar para cerrar una API.
Autenticación básica HTTP
Una de las opciones más simples en la Autenticación Básica de HTTP. Es increíblemente fácil de implementar en Rails y la mayoría de clientes no deberían tener ningún problema con ella. Para usarla sólo tenemos que modificar el controlador que sirve la API con una llamada a http_basic_authentication_with
, pasándole un nombre y una clave.
module Api module V1 class ProductsController < ApplicationController http_basic_authenticate_with name: "admin", password: "secret" respond_to :json # Actions omitten end end end
En una aplicación de verdad el nombre y la clave estarían en algún tipo de configuración externo de forma que no estarían guardados en control de versiones. Si tuviéramos que hacer esto en múltiples controladores tendríamos que moverlo todo a un nuevo controladory heredar nuestros controladores de él.
Podemos utilizar la orden curl
para probar este funcionamiento. Si hacemos una petición a la ruta de la API recibiremos un error.
$ curl http://localhost:3000/api/products HTTP Basic: Access denied.
Examinando las cabeceras de la respuesta veremos que hemos recibido un error 401 Unauthorized
.
$ curl http://localhost:3000/api/products -I HTTP/1.1 401 Unauthorized WWW-Authenticate: Basic realm="Application" Content-Type: text/html; charset=utf-8 X-UA-Compatible: IE=Edge Cache-Control: no-cache X-Request-Id: c411eeceefc39ab3964d40301530843c X-Runtime: 0.002366 Content-Length: 0 Connection: keep-alive Server: thin 1.3.1 codename Triple Espresso
Pero si pasamos el nombre y clave correctos recibiremos la respuesta JSON.
$ curl http://localhost:3000/api/products -u "admin:secret" [{"category_id":2,"created_at":"2012-05-30T20:16:58Z","id":1,"name":"Settlers of Catan","price":"29.95","released_on":"2012-04-12","updated_at":"2012-05-30T20:16:58Z"}, ...etc]
Tenemos que tener cuidado porque las credenciales se envían en texto claro por lo que deberíamos utilizar una conexión segura.
Autenticación mediante un token de acceso
Otra forma de cerrar la API es darle al cliente un token para acceder. Tenemos que almacenar este token en algún lugar, y en nuestro caso lo haremos en un nuevo modelo llamado api_key
.
$ rails g model api_key access_token
Hay otras columnas que le podemos añadir al modelo, por ejemplo se le puede añadir una columna role
para especificar los permisos que tiene este código, una columna user_id
para especificar el usuario al que pertenece, o incluso una columna expires_at
que indica el tiempo de validez. Por ahora nuestro modelo será muy sencillo, con tan sólo una columna Tenemos que ejecutar rake db:migrate
para crear la nueva tabla api_keys
.
En nuestro nuevo modelo tenemos que generar una cadena de acceso aleatoria cada vez que se crea un registro, lo que haremos con before_create
.
class ApiKey < ActiveRecord::Base before_create :generate_access_token private def generate_access_token begin self.access_token = SecureRandom.hex end while self.class.exists?(access_token: access_token) end end
Este código usa SecureRandom.hex
, que es una función de Ruby 1.9, que genera una cadena hexadecimal aleatoria. Luego comprueba si existe un código con la misma clave, en cuyo caso lo regenera para asegurar la unicidad (también podríamos haber puesto una restricción en base de datos). Podemos ver esto en acción con la consola, si invocamos a ApiKey.create!
generaremos un nuevo registro con un código aleatorio.
1.9.3-p125 :001 > ApiKey.create! (0.1ms) begin transaction ApiKey Exists (0.2ms) SELECT 1 FROM "api_keys" WHERE "api_keys"."access_token" = 'afbadb4ff8485c0adcba486b4ca90cc4' LIMIT 1 Binary data inserted for `string` type on column `access_token` SQL (5.9ms) INSERT INTO "api_keys" ("access_token", "created_at", "updated_at") VALUES (?, ?, ?) [["access_token", "afbadb4ff8485c0adcba486b4ca90cc4"], ["created_at", Wed, 30 May 2012 21:17:53 UTC +00:00], ["updated_at", Wed, 30 May 2012 21:17:53 UTC +00:00]] (2.7ms) commit transaction => #<ApiKey id: 1, access_token: "afbadb4ff8485c0adcba486b4ca90cc4", created_at: "2012-05-30 21:17:53", updated_at: "2012-05-30 21:17:53">
Nos toca decidir cuándo se genera el código y cómo se le muestra al cliente pero por lo general se hace en alguna página de perfil de forma que puedan copiarlo y pegarlo en la herramienta de cliente con la que deseen usar la API. Por nuestra parte, tenemos que restringir el acceso a la API exigiendo la presencia del código. Hay varias formas de hacerlo, una de ellas es añadirlo como parámetro en la URL, para lo que usaremos un before_filter
.
module Api module V1 class ProductsController < ApplicationController before_filter :restrict_access respond_to :json # Actions omitted private def restrict_access api_key = ApiKey.find_by_access_token(params[:access_token]) head :unauthorized unless api_key end end end end
En restrict_access
intentamos encontrar una ApiKey
mediante el access_token
que se ha recibido por la URL. Si no encontramos dicho registro devolveremos el error 401 Unauthorized
. Con esto siempre se verá una respuesta en blanco al cargar la página a no ser que se incluya un código válido de acceso.
Pero pasar el código de acceso mediante la URL no es por lo general la mejor solución, especialmente si el código no tiene expiración. La gente tiene tendencia a copiar y pegar URLs, y no queremos que accidentalmente compartan sus credenciales. En lugar de esto pasaremos el código de acceso mediante una cabecera HTTP. Rails hace que sea fácil añadir esta funcionalidad porque podemos usar authenticate_or_request_with_http_token
en un before_filter
. Tenemos ya un before_filter
en nuestro controlador ProductsController
, así que modificaremos el metodo restrict_access
para que utilice authenticate_or_request_with_http_token
.
def restrict_access authenticate_or_request_with_http_token do |token, options| ApiKey.exists?(access_token: token) end end
Si el bloque se le pasa a authenticate_or_request_with_http_token
devuelve true
la autenticación tiene éxito por lo que aquí comprobaremos si existe un registro de ApiKey
con el código recibido. Ahora se denegará el acceso a la API a no ser que se establezca correctamente la cabecera Authorization
:
$ curl http://localhost:3000/api/products -H 'Authorization: Token token="afbadb4ff8485c0adcba486b4ca90cc4"'
Podemos combinar todos estos esquemas de autenticación para que se adapten lo mejor posible a nuestra aplicación.
Las soluciones que hemos visto son bastante sencillas pero ¿qué pasa si se complica un poco la situación? Por ejemplo, ¿y si queremos que las aplicaciones que utilizan la API puedan ser capaces de simular el inicio de sesión como un usuario y acceder a sus credenciales, pero sólo si el usuario le da el permiso? Es un escenario bastante frecuente en aplicaciones de redes sociales como Facebook o Twitter, y la forma más correcta de hacer esto es con OAuth. No le dedicaremos mucho tiempo en este episodio a OAuth, pero se puede encontrar información detallada en su sitio web. En esencia nos permite asegurar una API y proteger los datos del usuario sin que tengan que guardar sus claves en todos los sitios en los que inician sesión.
Hay muchos proyectos que facilitan la implementación de OAuth en una aplicación Rails. Uno de ellos es Doorkeeper y si bien está en una etapa muy temprana de desarrollo merece la pena echarle un vistazo, que es lo que haremos en el episodio Pro de esta semana. También merece la pena investigar la gema oauth2 Muchos otros proyectos la usan por lo que es buena idea echarle un vistazo para saber cómo funciona.
Por supuesto, sea cual sea la solución por la que optemos, es muy importante que la interacción de la API se realice mediante una conexión segura SSL.