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.

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 el secreto de cliente en la aplicación nativa, pero las aplicaciones web no deben filtrar este valor.

Puedes rellenar cada fragmento de información que desees, excepto la dirección URL de devolución de llamada. Esta es la parte más importante para configurar de forma segura la aplicación. Es la URL de devolución de llamada a la que GitHub Enterprise Cloud devuelve al usuario después de autenticarse correctamente. La propiedad de esa dirección URL es lo que garantiza que los usuarios inicien sesión en la aplicación, en lugar de filtrar los tokens a un atacante.

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

Aceptar la autorización del usuario

Aviso de desuso: GitHub interrumpirá la autenticación a la API mediante parámetros de consulta. La autenticación en la API debe realizarse con la Autenticación HTTP básica. El uso de parámetros de consulta para autenticarse en la API ya no funcionará el 5 de mayo de 2021. Para obtener más información, incluidas las interrupciones programadas, vea la entrada de blog.

Ahora, vamos a comenzar a llenar nuestro servidor común. Cree un archivo denominado server.rb y péguelo en el servidor:

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 scope parámetro de consulta para definir los alcances solicitados por la aplicación. Para nuestra aplicación, solicitamos el alcance user:email para leer direcciones de correo electrónico privadas.

Vaya al explorador y vaya a http://127.0.0.1:4567. Después de hacer clic en el enlace, se te llevará a GitHub Enterprise Cloud y se te mostrará un diálogo de "Autorizar aplicación".

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

¿Se acuerda de cuándo especificamos la dirección URL de devolución de llamada como callback? No proporcionamos ninguna ruta, así que GitHub Enterprise Cloud no sabe dónde soltar al usuario después de autorizar la aplicación. ¡Arreglémoslo ahora!

Proporcionar un rellamado

En server.rb, agregue una ruta para especificar lo que debe hacer la devolución de llamada:

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 Enterprise Cloud proporciona un valor temporal para code. Tendrás que usar POST con este código para devolverlo a GitHub Enterprise Cloud con el secreto de cliente a cambio de un access_token. 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 "Ámbitos para las aplicaciones de OAuth".

Los alcances que otorgamos se devuelven como parte de la respuesta de intercambio de 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, usamos scopes.include? para comprobar si se ha concedido el alcance de user:email necesario para capturar 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 user, no se le podría haberse concedido explícitamente el ámbito user:email. En ese caso, recibiría un token con el ámbito user, que serviría para solicitar la dirección de correo electrónico del usuario, aunque no incluya user:email explícitamente en el token. La comprobación de user y user:email garantiza que se comprueban 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 API que esperaba que se realizaran correctamente podrían mostrar un error con el estado 404 o 401 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 X-OAuth-Scopesencabezado. 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('https://api.github.com/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('https://api.github.com/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 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 http://127.0.0.1:4567/basic. Obtendrás un error.

¿Qué pasaría si pudiéramos eludir todo el proceso de "hacer clic aquí" y simplemente recordarlo siempre que los usuarios estén conectados a GitHub Enterprise Cloud y así ellos podrían acceder a esta aplicación? Agárrese 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 rescue 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 X-OAuth-Scopes para verificar que el usuario no haya revocado el alcance user:email.

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('https://api.github.com/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('https://api.github.com/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, seguimos utilizando RestClient.get para llamar a la API de GitHub y aún estamos pasando los resultados para que se interpreten en una plantilla ERB (esta vez, se llama advanced.erb).

Además, ahora tenemos el método authenticated?, que comprueba si el usuario ya está autenticado. Si no, se llamará al método authenticate!, 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 ruby advanced_server.rb, que inicia el servidor en el puerto 4567, el mismo puerto que usamos cuando teníamos una aplicación sencilla de Sinatra. Al navegar a http://127.0.0.1:4567, la aplicación llama a authenticate!, que le redirige a /callback. A continuación, /callback nos envía de vuelta a / y, como nos hemos autenticado, se representa el archivo advanced.erb.

Podríamos simplificar completamente esta ruta de ida y vuelta solo con cambiar nuestra URL de devolución de llamada en GitHub Enterprise Cloud a /. Pero, dado que server.rb y advanced.rb se basan en la misma URL de devolución de llamada, tenemos que hacer un poco más de trabajo para que funcione.

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