I have a simple solution I've made myself with the following objects:
Account (has token field, that is returned when authenticating and used in API calls)
Authentication (has auth_type/auth_id and reference to Account)
I have a separate Authentication model to be able to connect several ways of login (device UUID, email/password, twitter, facebook etc). But it seems that in all examples of Devise you use it on the User (Account) model.
Isn't that less flexible? For example OmniAuth module stores provider and id on the User model, what happens if you want to be able to login from both Twitter and Facebook, there is only room for one provider?
Should I use Devise on my Account model or the Authentication model?
Have been recently working on a project where I was using Devise to keep user's tokens for different services. A bit different case, but still your question got me thinking for a while.
I'd bind Devise to Account model anyway. Why? Let's see.
Since my email is the only thing that can identify me as a user (and you refer to Account as the User) I would place it in accounts table in pair with the password, so that I'm initially able do use basic email/password authentication. Also I'd keep API tokens in authentications.
As you've mentioned, OmniAuth module needs to store provider and id. If you want your user to be able to be connected with different services at the same time (and for some reason you do) then obviously you need to keep both provider-id pairs somewhere, otherwise one will simply be overwritten each time a single user authenticates. That leads us to the Authentication model which is already suitable for that and has a reference to Account.
So when looking for a provider-id pair you want to check authentications table and not accounts. If one is found, you simply return an account associated with it. If not then you check if account containing such email exists. Create new authentication if the answer is yes, otherwise create one and then create authentication for it.
To be more specific:
#callbacks_controller.rb
controller Callbacks < Devise::OmniauthCallbacksContoller
def omniauth_callback
auth = request.env['omniauth.auth']
authentication = Authentication.where(provider: auth.prodiver, uid: auth.uid).first
if authentication
#account = authentication.account
else
#account = Account.where(email: auth.info.email).first
if #account
#account.authentication.create(provider: auth.provider, uid: auth.uid,
token: auth.credentials[:token], secret: auth.credentials[:secret])
else
#account = Account.create(email: auth.info.email, password: Devise.friendly_token[0,20])
#account.authentication.create(provider: auth.provider, uid: auth.uid,
token: auth.credentials[:token], secret: auth.credentials[:secret])
end
end
sign_in_and_redirect #account, :event => :authentication
end
end
#authentication.rb
class Authentication < ActiveRecord::Base
attr_accessible :provider, :uid, :token, :secret, :account_id
belongs_to :account
end
#account.rb
class Account < ActiveRecord::Base
devise :database_authenticatable
attr_accessible :email, :password
has_many :authentications
end
#routes.rb
devise_for :accounts, controllers: { omniauth_callbacks: 'callbacks' }
devise_scope :accounts do
get 'auth/:provider/callback' => 'callbacks#omniauth_callback'
end
That should give you what you need while keeping the flexibility you want.
You may separate all common logic to module and use only same table.
module UserMethods
#...
end
class User < ActiveRecord::Base
include UserMethods
devise ...
end
class Admin < ActiveRecord::Base
include UserMethods
self.table_name = "users"
devise ...
end
And configure all devise model separately in routes, views(if necessary, see Configuring Views). In this case, you may easily process all different logic.
Also note that if you are in a belief that devise is for user model only, then you are wrong.
For ex. - rails g devise Admin
This will create devise for admin model.
More information here.
Related
I'm using Rails 4 and Devise with Devise SAML Authenticatable for my Account system.
I've got the SAML working and all, but am trying to work out one thing.
I'd like to change one of the SAML attributes before saving it (since it is formatted incorrectly). Essentially, the Account's SAML request is given a role attribute which is one of the following Group_admin, Group_consumer, Group_supplier. I have a role field in my Account model enumerated as follows:
enum role: [:admin, :consumer, :supplier]
Clearly I can't directly set role because Group_admin != admin (etc.). Is there a way to modify the SAML attribute that is given before Devise saves the field?
I've tried a before_save filter to no avail.
before_save :fix_role!
private
def fix_role!
self.role = self.role.split('_')[1]
end
Does anyone know of a way to do this? I can post any other code if necessary, I'm just not sure what else is needed. Thanks.
I was able to do the following to fix the problem:
attribute-map.yml
"role": "full_role"
account.rb
before_save :set_role!
attr_accessor :full_role
private
def set_role!
self.role = self.full_role.split('_')[1]
end
Essentially, I used an attr_accessor to store the incorrectly formatted role given from the SAML response and a before_save filter to correctly set the "real" role field.
I want to permit users to sign up only if they have a token. I'm thinking the Confirmable class might be able to help me there. Any ideas?
Maybe a single administrator can register and send confirmations to invitees email addresses? Is there a gem or module around that has achieved something similar?
Use rails secure random to create invitations
Class Invitation
belongs_to :user #if your system has users
after_create :generate_token
# Add migration
# token :string
# sent_out :boolean, default: false
# used :boolean, default: false
def generate_token
self.token = SecureRandom.hex(13)
end
end
Just pseudocode but one way of doing it. Track when they are sent/used. Do a lookup to see if they are valid.
UPDATE
I have further clarified my question, listed at the end of this post.
Problem Summary:
I am trying to implement lazy (aka soft sign-up) registration in Devise via an emailed URL which includes token authentication. On my site, a User has_many :payments, and a Payment belongs_to :user. When a User creates a new Payment, it includes the attribute :email which represents a new non-registered user (which I'll call "Guest"). I use ActionMailer to send an email to this new Guest.
In this email that is sent, I would like to include a URL with token authentication (e.g. http://localhost/index?auth_token=TOKENVALUE), so that the Guest can see and edit views that require authentication and are specifically customized to them (based on their :email). The Guest should also have the ability to register as a User - since I already have their email address, they would just need to provide a password.
My Progress So Far:
I have implemented the ability for someone to register for the site using Devise and created the associated views, model and controller to make changes to my Payment model
I have setup ActionMailer and am using it to send emails, but have not yet setup token-authentication as I'm not sure how to do so given my use case above
Related Resources:
https://github.com/plataformatec/devise/wiki/How-To:-Simple-Token-Authentication-Example
http://zyphdesignco.com/blog/simple-auth-token-example-with-devise
Multiple user models with Ruby On Rails and devise to have separate registration routes but one common login route
how to create a guest user in Rails 3 + Devise
https://github.com/plataformatec/devise/wiki/How-To:-Create-a-guest-user
http://blog.bignerdranch.com/1679-lazy-user-registration-for-rails-apps/
http://railscasts.com/episodes/393-guest-user-record?view=asciicast
https://github.com/plataformatec/devise/wiki/How-To:-Manage-users-through-a-CRUD-interface
Rails 3 - Devise Gem - How to Manage Users through CRUD interface
http://danielboggs.com/articles/rails-authentication-and-user-management-via-crud/
/app/models/payment.rb
class Payment < ActiveRecord::Base
attr_accessible :amount, :description, :email, :frequency, :paid, :user_id
belongs_to :user
validates :email, :presence => true, :format => { :with => /.+#.+\..+/i }
validates :amount, :presence => true
validates :description, :presence => true
end
/app/models/user.rb
class User < ActiveRecord::Base
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable, :confirmable
# I will also need to add ":token_authenticatable"
attr_accessible :email, :password, :password_confirmation, :remember_me
has_many :payments
end
Questions:
How can I keep my User table in sync with new Payments that are created, such that any new :email created in the Payment table automatically are added in the User table as an :email?
Following from question 1, the new User created should include a token but not include a password, since the User has not yet registered for the site. Should I create the new User with a blank password, randomly generated string, or other? In either case, I think they would need to come to the registration page from their token-authenticated URL to prevent others from registering under their email username (e.g. lazy registration)
Following from question 2, I think that I will need to distinguish between Guests and normal users, as some of the views will change depending on user type. Is there a preferred method other than adding a column that would have a 0 or 1 to delineate between the two user types?
My preference if possible is to use Devise since I am using many of the features included. I'm new to RoR and appreciate any advice you can provide!
EDIT: Here is the code I used to address question #1 above, in my payments controller, in case helpful to someone else
def create
#payment = current_user.payments.build(params[:payment])
#Here is the code I added to keep User :email in sync with Payment :email, without token authentication implemented yet
unless User.find_by_email(params[:payment][:email].downcase)
u = User.new({:email => params[:payment][:email].downcase, :password => nil, :password_confirmation => nil })
u.skip_confirmation!
u.save(:validate => false) #skip validation
end
respond_to do |format|
if #payment.save
format.html { redirect_to payments_url, :flash => { notice: 'Payment was successfully created.' } }
format.json { render json: #payment, status: :created, location: #payment }
else
format.html { render action: "new" }
format.json { render json: #payment.errors, status: :unprocessable_entity }
end
end
end
Frameworks like Devise are great when you know how to use them well and what you want to do fits within what the framework was designed to do. When you start trying to "bend" them to do things they weren't designed to do, though, perhaps trying to monkey-patch them without fully understanding how they work internally, in my experience, you just make a mess. I've been there. Often you are reduced to just "hacking until it works"... which really means "hacking until it appears to work, aside from a dozen subtle bugs which only show up later".
In a case like this, I would be inclined to just "roll my own" login and authentication code, perhaps looking at the Devise source code for ideas.
(In fact, I did just that today on a new Rails app... it took a couple hours of hours, checking the Devise source for ideas now and again, to write the 200 lines of code my project actually needed. Devise itself, in comparison, has about 2500 lines.)
You asked for ideas on "overall strategy and structure" of the code. If you decide to implement your own custom login/authentication code, it will probably go something like this:
Store some information in the session hash to identify if the user is logged in, and as which Guest/User. Make sure your sessions are stored in the DB, not in cookies.
Add some methods to ApplicationController (or a Module mixed in to ApplicationController) to encapsulate access to this login information.
Put a before_filter on all pages which require login, which redirects if the user is not logged in.
Add a controller which authenticates users by password OR token, and allows them to log in/out.
For password storage/authentication, use the bcrypt-ruby gem. On your User model, you'll need a password_hash string field and probably some methods like:
require 'bcrypt'
def password
# BCrypt handles generation and storage of salts
#password ||= ::BCrypt::Password.new(password_hash)
end
def password=(password)
#password = ::BCrypt::Password.create(password)
self.password_hash = #password
end
def authenticate_by_password(password)
self.password == password
end
For token-based authentication, add a login_token string field to your User model (or Guest or whatever it is...). Make it a unique field. You can generate the unique tokens using something like this (borrowed from Devise and modified a little):
require 'securerandom'
def generate_login_token
loop do
token = SecureRandom.base64(15).tr('+/=lIO0', 'pqrsxyz')
unless User.where(:login_token => token).exists?
self.login_token = token
self.save!
return
end
end
end
Then when a user hits your login action with params[:token] set, just do something like:
if user = User.find_by_login_token(params[:token])
# if the token is supposed to be "one time only", null out the attribute
# log user in
else
# login token was invalid
end
Make sure you put a DB index on the login_token field.
Is there anything else?
I know I am not suppose to do this, but I dont know how else should I do this.
I want to use different database based on which user loged in. So I thought best way would be if I set up a session variable on first user login...
this is how it looks like:
class Stuff < ActiveRecord::Base
establish_connection(
:adapter => "mysql2",
:host => "127.0.0.1",
:username => session["dbuser"],
:password => session["dbuserpass"],
:database => session["dbname"])
and this of course does not work. Does anyone know how to do this?
Thank you.
You can rewrite your method as:
class Stuff < ActiveRecord::Base
def establish_connection_user(user, pass, database)
establish_connection(
:adapter => "mysql2",
:host => "127.0.0.1",
:username => user,
:password => pass,
:database => database)
end
end
and in your controller:
class StuffController < ApplicationController
def login #example
stuff = Stuff.new
stuff.establish_connection_user(
session[:dbuser],
session[:dbuserpass],
session[:dbname])
end
This way you also encapsulate it and make the details less obvious. I suggest you also
encrypt your cookie so you don't have the credentials so exposed. You can take an idea
from this answer:
Storing an encrypted cookie with Rails
You can select the database in your model like this:
establish_connection "db_name_#{session[:something]}"
This way your model know which database to pull/push data.
Look at this: http://m.onkey.org/how-to-access-session-cookies-params-request-in-model
As hinted by kwon in the comments on the original question, I would approach it through using the session only to retain the identity of the user. I would then pull the desired database connection from logic in a model and a central database (the default rails DB) persisting user details and user connection information.
Start with a modification to your user model (assuming that your user has a model that is persisted in a central database)
add an attribute to the user representing the data to be used
in your application controller set the user in a before_filter, based on the session key
initialize your Stuff model with a user argument
You can then lookup your database connection based on the database.yml. Or if you have one database per user and you need this to be dynamic, create a second model (in the central database) representing the database connection with a foreign key onto the user model.
The following is a bunch of code that may or may not work in reality, but hopefully gives you a template for getting started.
class ApplicationController < ActionController::Base
before_filter :set_user
def set_user
begin
#user = UserProfile.find(session[:usernumber]) if session[:usernumber]
rescue
logger.warn "Possible error in set_user. Resetting session: #{$!}"
#user=nil
session[:usernumber]=nil
reset_session
end
end
end
class StuffController < ApplicationController
def show
#stuff = Stuff.user_get(#user, params[:id])
end
end
class Stuff < ActiveRecord::Base
# This would be better moved to a module to reuse across models
def self.establish_connection_user(user)
establish_connection(user.connection_hash)
end
def establish_connection_user(user)
establish_connection(user.connection_hash)
end
def self.user_get user, item_id
establish_connection_user(user)
find(id)
end
def self.user_where user, *query_args
establish_connection_user(user)
where(query_args)
end
# Even better than replicating 'where', create model methods
# that are more representative of your desired functionality
end
class User < ActiveRecord::Base
has_one :user_connection
def connection_hash
uc = self.user_connection
{:database=>uc.db, :password=>uc.pass, :user=>uc.username, :host=>uc.dbhost, :adapter=>uc.adapter}
end
# User probably contains other user-facing details
end
If you have the option of using PostgreSQL you can use the Schemas feature of PostgreSQL to effectively have separate table namespaces (schemas) for each user. The benefit here is you are still connected to the same database (thus avoiding hacking up the rails API), but you get the same benefits of multiple DBs in terms of database separation.
If you have a RailsCasts Pro subscription ($9/mo) Ryan Bates has an excellent video on the subject: http://railscasts.com/episodes/389-multitenancy-with-postgresql
Jerod Santo also did a great write up on his blog: http://blog.jerodsanto.net/2011/07/building-multi-tenant-rails-apps-with-postgresql-schemas/
In both examples they use subdomains to switch between tenants/schemas but you could easily link it to a user record.
I've setup Devise (on Rails 3) to use Basecamp-style subdomain authentication. Under this model, a user could be registered twice under different subdomains with the same email address.
For example:
class User < ActiveRecord::Base
belongs_to :account
end
class Account < ActiveRecord::Base
# subdomain attribute stored here
end
User 1 registered on company1.myapp.com with email address bob#acme.com
User 2 registered on company2.myapp.com with email address bob#acme.com
(Both user account are controlled by the same human, but belong to different subdomains.)
Logging in works fine, but the standard Password Reset only looks up by email address, so you can only ever reset the password for User 1. What I'd like to do is take into account the request subdomain, so a password reset from company2.myapp.com/password/new would reset the password for User 2.
The Devise looks up the user using a find_first method, which I don't think accepts joins, so I can't include a :account => {:subodmain => 'comapny2'} condition.
I can reimplement send_reset_password_instructions to manually look up the user record, but it feels hacky and I'll need to do it for send_confirmation_instructions, too.
Is there a better way?
It looks like this may be configurable with devise_for in the routes file.
From my reading of the source (and I haven't actually tried this), you can add a reset_password_keys option. These should include the subdomain. This is passed to find_or_initialize_with_errors from send_reset_password_instructions in lib/devise/models/recoverable.rb. In find_or_initialize_with_errors it's only these keys which are used to find the resource.
You'll probably also want to override Devise::PasswordsController#new template to include the user's subdomain when they submit the reset password request.
UPDATE: to address the fact that the subdomain is stored on Account and User belongs_to :account you can probably use Rails' delegate method.
We experienced this same issue. Mike Mazur's answer worked, but for one difference:
We put :reset_password_keys => [:email, :subdomain] in the call to the devise method in our Users model.
I recently implement this behaviour in a Rails 4 App.
…/config/initializers/devise.rb
(…)
# ==> Configuration for :recoverable
#
# Defines which key will be used when recovering the password for an account
config.reset_password_keys = [:email, :subdomain]
(…)
…/app/views/devise/passwords/new.html.erb
(…)
<%= f.input :subdomain, required: true %>
(…)
…/app/controllers/users/passwords_controller.rb
class Users::PasswordsController < Devise::PasswordsController
def resource_params
params.require(:user).permit(:email, :subdomain, ...)
end
private :resource_params
end