ruby on rails - Autenticación de usuario con uva y diseño
ruby-on-rails api (2)
Agregue token_authenticable para diseñar módulos (funciona con versiones de diseño <= 3.2)
En user.rb agregue: token_authenticatable a la lista de módulos de diseño, debería verse algo como a continuación:
class User < ActiveRecord::Base
# ..code..
devise :database_authenticatable,
:token_authenticatable,
:invitable,
:registerable,
:recoverable,
:rememberable,
:trackable,
:validatable
attr_accessible :name, :email, :authentication_token
before_save :ensure_authentication_token
# ..code..
end
Generar un token de autenticación por su cuenta (si es una versión> 3.2)
class User < ActiveRecord::Base
# ..code..
devise :database_authenticatable,
:invitable,
:registerable,
:recoverable,
:rememberable,
:trackable,
:validatable
attr_accessible :name, :email, :authentication_token
before_save :ensure_authentication_token
def ensure_authentication_token
self.authentication_token ||= generate_authentication_token
end
private
def generate_authentication_token
loop do
token = Devise.friendly_token
break token unless User.where(authentication_token: token).first
end
end
Agregar migración para token de autentificación
rails g migration add_auth_token_to_users
invoke active_record
create db/migrate/20141101204628_add_auth_token_to_users.rb
Edite el archivo de migración para agregar: columna authentication_token a los usuarios
class AddAuthTokenToUsers < ActiveRecord::Migration
def self.up
change_table :users do |t|
t.string :authentication_token
end
add_index :users, :authentication_token, :unique => true
end
def self.down
remove_column :users, :authentication_token
end
end
Ejecutar migraciones
rake db:migrate
Generar token para usuarios existentes
Necesitamos llamar a guardar en cada instancia de usuario que garantizará que el token de autenticación esté presente para cada usuario.
User.all.each(&:save)
API segura de uva utilizando token de autenticación
Debe agregar el código siguiente a API :: Root para agregar la autenticación basada en token. Si no está al tanto de API :: Root, por favor lea Building RESTful API using Grape
En el ejemplo siguiente, estamos autenticando al usuario según dos escenarios: si el usuario ha iniciado sesión en la aplicación web, utilice la misma sesión: si la sesión no está disponible y se pasa el token de autorización, busque el usuario en función del token
# lib/api/root.rb
module API
class Root < Grape::API
prefix ''api''
format :json
rescue_from :all, :backtrace => true
error_formatter :json, API::ErrorFormatter
before do
error!("401 Unauthorized", 401) unless authenticated
end
helpers do
def warden
env[''warden'']
end
def authenticated
return true if warden.authenticated?
params[:access_token] && @user = User.find_by_authentication_token(params[:access_token])
end
def current_user
warden.user || @user
end
end
mount API::V1::Root
mount API::V2::Root
end
end
Tengo dificultades para comprender y también implementar correctamente la Autenticación de usuario en las API. En otras palabras, tengo un serio problema para entender la integración de la API de Grape con frameworks frontales como Backbone.js, AngularJS o Ember.js.
Estoy tratando de hacer pivotar todos los enfoques diferentes y leer mucho sobre eso, pero Google me devuelve recursos realmente malos y me parece que no hay un artículo realmente bueno sobre este tema: Rails y autenticación de usuario con Devise y front-end marcos .
Describiré mi pivote actual y espero que puedan brindarme algunos comentarios sobre mi implementación y tal vez apuntarme en la dirección correcta.
Implementación actual
Tengo la API REST de Rails de respaldo con el siguiente Gemfile (voy a acortar deliberadamente todo el código de archivo)
gem ''rails'', ''4.1.6''
gem ''mongoid'', ''~> 4.0.0''
gem ''devise''
gem ''grape''
gem ''rack-cors'', :require => ''rack/cors''
Mi implementación actual solo tiene API con las siguientes rutas ( routes.rb ):
api_base /api API::Base
GET /:version/posts(.:format)
GET /:version/posts/:id(.:format)
POST /:version/posts(.:format)
DELETE /:version/posts/:id(.:format)
POST /:version/users/authenticate(.:format)
POST /:version/users/register(.:format)
DELETE /:version/users/logout(.:format)
Creé tener el siguiente modelo user.rb
class User
include Mongoid::Document
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
field :email, type: String, default: ""
field :encrypted_password, type: String, default: ""
field :authentication_token, type: String
before_save :ensure_authentication_token!
def ensure_authentication_token!
self.authentication_token ||= generate_authentication_token
end
private
def generate_authentication_token
loop do
token = Devise.friendly_token
break token unless User.where(authentication_token: token).first
end
end
end
En mis controladores creé la siguiente estructura de carpetas: controladores-> api-> v1 y he creado la siguiente autenticación de módulo compartido ( authentication.rb )
module API
module V1
module Authentication
extend ActiveSupport::Concern
included do
before do
error!("401 Unauthorized", 401) unless authenticated?
end
helpers do
def warden
env[''warden'']
end
def authenticated?
return true if warden.authenticated?
params[:access_token] && @user = User.find_by(authentication_token: params[:access_token])
end
def current_user
warden.user || @user
end
end
end
end
end
end
Así que cada vez que quiero asegurarme de que se invocará mi recurso con el token de autenticación, simplemente puedo agregarlo llamando al: include API::V1::Authentication
al recurso de uva:
module API
module V1
class Posts < Grape::API
include API::V1::Defaults
include API::V1::Authentication
Ahora tengo otro recurso de Grape llamado Users (users.rb) y aquí implemento métodos para autenticación, registro y cierre de sesión. (Creo que mezclo aquí manzanas con peras, y debería extraer el proceso de inicio de sesión / cierre de sesión en otro recurso de Grape - Sesión).
module API
module V1
class Users < Grape::API
include API::V1::Defaults
resources :users do
desc "Authenticate user and return user object, access token"
params do
requires :email, :type => String, :desc => "User email"
requires :password, :type => String, :desc => "User password"
end
post ''authenticate'' do
email = params[:email]
password = params[:password]
if email.nil? or password.nil?
error!({:error_code => 404, :error_message => "Invalid email or password."}, 401)
return
end
user = User.find_by(email: email.downcase)
if user.nil?
error!({:error_code => 404, :error_message => "Invalid email or password."}, 401)
return
end
if !user.valid_password?(password)
error!({:error_code => 404, :error_message => "Invalid email or password."}, 401)
return
else
user.ensure_authentication_token!
user.save
status(201){status: ''ok'', token: user.authentication_token }
end
end
desc "Register user and return user object, access token"
params do
requires :first_name, :type => String, :desc => "First Name"
requires :last_name, :type => String, :desc => "Last Name"
requires :email, :type => String, :desc => "Email"
requires :password, :type => String, :desc => "Password"
end
post ''register'' do
user = User.new(
first_name: params[:first_name],
last_name: params[:last_name],
password: params[:password],
email: params[:email]
)
if user.valid?
user.save
return user
else
error!({:error_code => 404, :error_message => "Invalid email or password."}, 401)
end
end
desc "Logout user and return user object, access token"
params do
requires :token, :type => String, :desc => "Authenticaiton Token"
end
delete ''logout'' do
user = User.find_by(authentication_token: params[:token])
if !user.nil?
user.remove_authentication_token!
status(200)
{
status: ''ok'',
token: user.authentication_token
}
else
error!({:error_code => 404, :error_message => "Invalid token."}, 401)
end
end
end
end
end
end
Me doy cuenta de que presento aquí un montón de código y puede que no tenga sentido, pero esto es lo que tengo actualmente y puedo usar el authentication_token
para las llamadas en mi API que están protegidas por la Authentication
módulo.
Siento que esta solución no es buena, pero realmente busco la forma más fácil de lograr la autenticación de usuario a través de API. Tengo varias preguntas que enumero a continuación.
Preguntas
- ¿Crees que este tipo de implementación es peligrosa, de ser así, por qué? - Creo que lo es, debido al uso de un token. ¿Hay alguna manera de mejorar este patrón? También he visto la implementación con
Token
modelo separado que tiene tiempo de vencimiento, etc. Pero creo que esto es casi como reinventar la rueda, porque para este propósito puedo implementar OAuth2. Me gustaría tener una solución más ligera. - Es una buena práctica crear un nuevo módulo para Autenticación e incluirlo solo en los recursos donde sea necesario.
- ¿Conoces algún buen tutorial sobre este tema: implementar Rails + Devise + Grape? Además, ¿conoces algún buen proyecto de código abierto de Rails, que se implementa de esta manera?
- ¿Cómo puedo implementarlo con un enfoque diferente que sea más seguro?
Me disculpo por una publicación tan larga, pero espero que más personas tengan el mismo problema y pueda ayudarme a encontrar más respuestas a mis preguntas.
Aunque me gusta la pregunta y la respuesta de @MZaragoza, creo que vale la pena señalar que token_authentical ha sido eliminado de Devise por algún motivo. El uso de los tokens es vulnerable a los ataques de tiempo. Ver también esta publicación y el blog de Devise. Por lo tanto, no he votado a favor la respuesta de @MZaragoza.
En caso de que use su API en combinación con Doorkeeper, podría hacer algo similar, pero en lugar de buscar el authentication_token en la tabla / modelo de usuario, busca el token en la tabla OauthAccessTokens, es decir,
def authenticated
return true if warden.authenticated?
params[:access_token] && @user = OauthAccessToken.find_by_token(params[:access_token]).user
end
Esto es más seguro, porque ese token (es decir, el access_token real) existe solo por una cierta cantidad de tiempo.
Para poder hacer esto, debe tener un modelo de Usuario y un modelo de OauthAccessToken, con:
class User < ActiveRecord::Base
has_many :oauth_access_tokens
end
class OauthAccessToken < ActiveRecord::Base
belongs_to :user, foreign_key: ''resource_owner_id''
end
EDITAR: Tenga en cuenta que generalmente no debe incluir access_token en la URL: http://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-16#section-2.3