Skip to main content

Autenticación en la API REST con una aplicación de OAuth

Aprende acerca de las formas diferentes de autenticarse con algunos ejemplos.

En esta sección, vamos a enfocarnos en lo básico de la autenticación. En concreto, vamos a crear un servidor de Ruby (mediante Sinatra) que implemente el flujo web de una aplicación de varias maneras diferentes.

Sugerencia

Puede descargar el código fuente completo de este proyecto desde el repositorio platform-samples.

Registrar la aplicación

En primer lugar, debe registrar la aplicación. A cada OAuth app que se registra se le asigna un id. de cliente y un secreto de cliente únicos. El secreto de cliente se usa para obtener un token de acceso para el usuario que ha iniciado sesión. Debes incluir la clave secreta del cliente en la aplicación nativa, pero las aplicaciones web no deben revelar este valor.

Puedes rellenar cada fragmento de información que desees, excepto la URL de redirección de autorización. Esta es la parte más importante para configurar de forma segura la aplicación. Es la URL de callback a la que GitHub redirige al usuario tras una autenticación exitosa. La propiedad de esa dirección URL es lo que garantiza que los usuarios inicien sesión en tu aplicación, en lugar de exponer los tokens a un atacante.

Como ejecutamos un servidor habitual de Sinatra, la ubicación de la instancia local está configurada en . Vamos a rellenar la URL de devolución de llamada como .

Aceptar la autorización del usuario

Advertencia

Retirado Aviso: La autenticación en la API GitHub ya no es accesible mediante parámetros de consulta. La autenticación en la API debe realizarse con autenticación básica HTTP. Para obtener más información, incluidas las interrupciones programadas, consulta la entrada de blog. La autenticación a la API mediante parámetros de búsqueda, si bien está disponible, ya no es compatible por motivos de seguridad. En su lugar, se recomienda que los integradores muevan su token de acceso, client_id o client_secret en el encabezado. GitHub notificará sobre la eliminación de la autenticación por parámetros de consulta con tiempo suficiente.

Ahora, vamos a comenzar a llenar nuestro servidor simple. Cree un archivo denominado server.rb y péguelo en él:

require 'sinatra'
require 'rest-client'
require 'json'

CLIENT_ID = ENV['GH_BASIC_CLIENT_ID']
CLIENT_SECRET = ENV['GH_BASIC_SECRET_ID']

get '/' do
  erb :index, :locals => {:client_id => CLIENT_ID}
end

El identificador de cliente y el secreto de cliente proceden de la página de configuración de la aplicación. Se recomienda almacenar estos valores como variables de entorno para facilitar el reemplazo y el uso, que es exactamente lo que hemos hecho aquí.

A continuación, en views/index.erb, pegue este contenido:

<html>
  <head>
  </head>
  <body>
    <p>
      Well, hello there!
    </p>
    <p>
      We're going to now talk to the GitHub API. Ready?
      <a href="https://github.com/login/oauth/authorize?scope=user:email&client_id=<%= client_id %>">Click here</a> to begin!
    </p>
    <p>
      If that link doesn't work, remember to provide your own <a href="/apps/building-oauth-apps/authorizing-oauth-apps/">Client ID</a>!
    </p>
  </body>
</html>

(Si no está familiarizado con el funcionamiento de Sinatra, le recomendamos leer la guía de Sinatra).

Además, tenga en cuenta que la dirección URL usa el parámetro de consulta para definir los alcances solicitados por la aplicación. Para nuestra aplicación, solicitamos el alcance para leer direcciones de correo electrónico privadas.

Abra su navegador y diríjase a . Después de hacer clic en el enlace, se te llevará a GitHub y se te mostrará un cuadro de diálogo "Autorizar aplicación".

Si confía en usted mismo, haga clic en Authorize App. ¡Oh-oh! Sinatra genera un error. ¡¿Qué pasa?!

¿Se acuerda de cuándo especificamos una Callback URL como ? No se le ha proporcionado ninguna ruta, así que GitHub no sabe dónde enviar al usuario después de autorizar la aplicación. ¡Arreglémoslo ahora!

Proporcionar un retorno de llamada

En el server.rb, agregue una ruta para especificar lo que debe hacer el callback.

get '/callback' do
  # get temporary GitHub code...
  session_code = request.env['rack.request.query_hash']['code']

  # ... and POST it back to GitHub
  result = RestClient.post('https://github.com/login/oauth/access_token',
                          {:client_id => CLIENT_ID,
                           :client_secret => CLIENT_SECRET,
                           :code => session_code},
                           :accept => :json)

  # extract the token and granted scopes
  access_token = JSON.parse(result)['access_token']
end

Cuando una aplicación se autentica correctamente, GitHub proporciona un valor temporal. Tendrás que enviar este código de vuelta a GitHub junto con tu secreto de cliente a cambio de una instancia de . Para simplificar nuestras solicitudes HTTP GET y POST, usamos rest-client. Nota que probablemente jamás accedas a la API a través de REST. Si va a hacer un uso más intensivo, probablemente debería usar una biblioteca escrita en el idioma que prefiera.

Verificar los alcances otorgados

Los usuarios pueden editar los alcances que solicitaste cambiando directamente la URL. Esto puee otorgar a tu aplicación menos accesos de los que solicitaste originalmente. Antes de hacer cualquier solicitud con el token, verifica los alcances que el usuario le otorgó a éste. Para más información sobre los alcances solicitados y concedidos, consulta AUTOTITLE.

Los alcances que fueron otorgados se devuelven como parte de la respuesta al intercambiar un token.

get '/callback' do
  # ...
  # Get the access_token using the code sample above
  # ...

  # check if we were granted user:email scope
  scopes = JSON.parse(result)['scope'].split(',')
  has_user_email_scope = scopes.include? 'user:email' || scopes.include? 'user'
end

En nuestra aplicación, analizamos si se nos ha otorgado el ámbito necesario para obtener las direcciones de correo electrónico privadas del usuario autenticado. Si la aplicación hubiera preguntado por otros alcances, las habríamos comprobado también.

Además, dado que hay una relación jerárquica entre ámbitos, debes comprobar que se te haya otorgado alguno de los niveles más altos del ámbito requerido. Por ejemplo, si la aplicación hubiera solicitado el ámbito , no se le podría haberse concedido explícitamente el ámbito . En ese caso, recibiría un token con el ámbito especificado, que serviría para solicitar la dirección de correo electrónico del usuario, aunque no lo incluya explícitamente en el token. Comprobar tanto como asegura que se verifican ambos escenarios.

Comprobar los alcances solo antes de realizar las solicitudes no es suficiente, ya que es posible que los usuarios cambien los alcances entre la comprobación y la solicitud real. En caso de que esto suceda, las llamadas a la API que se esperaba que tuvieran éxito podrían fallar mostrando un estado de error o devolver un subconjunto diferente de información.

Para ayudarle a controlar estas situaciones correctamente, todas las respuestas de API de las solicitudes realizadas con tokens válidos también contienen un encabezado. Este encabezado contiene la lista de alcances del token que se utilizó para realizar la solicitud. Además de eso, la API de REST proporciona un punto de conexión para comprobar si hay un token de validez. Utilice esta información para detectar cambios en los alcances de los tokens y para informar a los usuarios sobre los cambios disponibles en la funcionalidad de la aplicación disponible.

Realizar solicitudes autenticadas

Por último, con este token de acceso, podrá realizar solicitudes autenticadas como el usuario que inició sesión:

# fetch user information
auth_result = JSON.parse(RestClient.get('http(s)://HOSTNAME/api/v3/user',
                                        {:params => {:access_token => access_token}}))

# if the user authorized it, fetch private emails
if has_user_email_scope
  auth_result['private_emails'] =
    JSON.parse(RestClient.get('http(s)://HOSTNAME/api/v3/user/emails',
                              {:params => {:access_token => access_token}}))
end

erb :basic, :locals => auth_result

Podemos hacer lo que queramos con nuestros resultados. En este caso, los volcaremos directamente en el basic.erb.

<p>Hello, <%= login %>!</p>
<p>
  <% if !email.nil? && !email.empty? %> It looks like your public email address is <%= email %>.
  <% else %> It looks like you don't have a public email. That's cool.
  <% end %>
</p>
<p>
  <% if defined? private_emails %>
  With your permission, we were also able to dig up your private email addresses:
  <%= private_emails.map{ |private_email_address| private_email_address["email"] }.join(', ') %>
  <% else %>
  Also, you're a bit secretive about your private email addresses.
  <% end %>
</p>

Implementar la autenticación "persistente"

No es muy buena idea exigir que los usuarios inicien sesión en la aplicación cada vez que necesiten acceder a la página web. Por ejemplo, intente navegar directamente a . Recibirás un error.

¿Qué pasaría si pudiéramos eludir todo el proceso de "hacer clic aquí" y simplemente recordar que, cuando los usuarios estén conectados a GitHub deberían poder acceder a esta aplicación? Agárrese el sombrero, porque eso es exactamente lo que vamos a hacer.

Nuestro pequeño servidor que mostramos antes es muy simple. Para poder insertar algún tipo de autenticación inteligente, vamos a optar por utilizar sesiones para almacenar los tokens. Esto hará que la autenticación sea transparente para el usuario.

Ya que estamos usando los mismos alcances dentro de la sesión, necesitaremos gestionar los casos cuando el usuario actualice los alcances después de que los comprobemos, o cuando se revoque el token. Para lograrlo, utilizaremos un bloque de y verificaremos que la primera llamada a la API se realizó correctamente, lo cual confirmará que el token sigue siendo válido. Después de eso, comprobaremos el encabezado de respuesta para verificar que el usuario no haya revocado el alcance .

Cree un archivo denominado advanced_server.rb y pegue estas líneas en él:

require 'sinatra'
require 'rest_client'
require 'json'

# Don't use hard-coded values in your app
# Instead, set and test environment variables, like below
# if ENV['GITHUB_CLIENT_ID'] && ENV['GITHUB_CLIENT_SECRET']
#  CLIENT_ID        = ENV['GITHUB_CLIENT_ID']
#  CLIENT_SECRET    = ENV['GITHUB_CLIENT_SECRET']
# end

CLIENT_ID = ENV['GH_BASIC_CLIENT_ID']
CLIENT_SECRET = ENV['GH_BASIC_SECRET_ID']

use Rack::Session::Pool, :cookie_only => false

def authenticated?
  session[:access_token]
end

def authenticate!
  erb :index, :locals => {:client_id => CLIENT_ID}
end

get '/' do
  if !authenticated?
    authenticate!
  else
    access_token = session[:access_token]
    scopes = []

    begin
      auth_result = RestClient.get('http(s)://HOSTNAME/api/v3/user',
                                   {:params => {:access_token => access_token},
                                    :accept => :json})
    rescue => e
      # request didn't succeed because the token was revoked so we
      # invalidate the token stored in the session and render the
      # index page so that the user can start the OAuth flow again

      session[:access_token] = nil
      return authenticate!
    end

    # the request succeeded, so we check the list of current scopes
    if auth_result.headers.include? :x_oauth_scopes
      scopes = auth_result.headers[:x_oauth_scopes].split(', ')
    end

    auth_result = JSON.parse(auth_result)

    if scopes.include? 'user:email'
      auth_result['private_emails'] =
        JSON.parse(RestClient.get('http(s)://HOSTNAME/api/v3/user/emails',
                       {:params => {:access_token => access_token},
                        :accept => :json}))
    end

    erb :advanced, :locals => auth_result
  end
end

get '/callback' do
  session_code = request.env['rack.request.query_hash']['code']

  result = RestClient.post('https://github.com/login/oauth/access_token',
                          {:client_id => CLIENT_ID,
                           :client_secret => CLIENT_SECRET,
                           :code => session_code},
                           :accept => :json)

  session[:access_token] = JSON.parse(result)['access_token']

  redirect '/'
end

La mayoría de este código debería serte familiar. Por ejemplo, todavía estamos utilizando para llamar a la API de GitHub, y todavía pasamos nuestros resultados para que se rendericen en una plantilla ERB (en esta ocasión, se denomina ).

Además, ahora tenemos el método , que comprueba si el usuario ya está autenticado. Si no, se llamará al método , el cual lleva a cabo el flujo de OAuth y actualiza la sesión con el token y los alcances que se otorgaron.

A continuación, cree un archivo en views denominado advanced.erb y pegue este marcado en él:

<html>
  <head>
  </head>
  <body>
    <p>Well, well, well, <%= login %>!</p>
    <p>
      <% if !email.empty? %> It looks like your public email address is <%= email %>.
      <% else %> It looks like you don't have a public email. That's cool.
      <% end %>
    </p>
    <p>
      <% if defined? private_emails %>
      With your permission, we were also able to dig up your private email addresses:
      <%= private_emails.map{ |private_email_address| private_email_address["email"] }.join(', ') %>
      <% else %>
      Also, you're a bit secretive about your private email addresses.
      <% end %>
    </p>
  </body>
</html>

Desde la línea de comandos, llame a , que inicia el servidor en el puerto , el mismo puerto que usamos cuando teníamos una aplicación sencilla de Sinatra. Al navegar a , la aplicación llama a , lo que le redirige a . A continuación, nos envía de vuelta a y, como nos hemos autenticado, se representa el archivo advanced.erb.

Podríamos simplificar completamente este enrutamiento de ida y vuelta simplemente cambiando nuestra URL de devolución de llamada en GitHub a . Pero, dado que server.rb y advanced.rb utilizan la misma URL de devolución de llamada, tenemos que hacer algunas maniobras para que funcione.

Además, si nunca hubiéramos autorizado esta aplicación para acceder a nuestros datos de GitHub, habríamos visto el mismo cuadro de diálogo de confirmación del elemento emergente anterior a modo de advertencia.