Modify Devise SAML Attributes - ruby-on-rails

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.

Related

Setting default username in Rails

I am using devise gem for user management. So, I have a User model.
What I want to be able to do is have a username column in the User model. By default, I want to be able to set the default username for users as 'user'+id where id is unique for every user and a column in User model.
I want to be able to do something like:
class AddColsToUser < ActiveRecord::Migration[6.1]
def change
add_column :users, :username, :string, default: 'user'+id
end
end
Is that possible? Thanks for reading :))
I would recommend using a before_validation callback to create a new username. You can expand this a bit to keep retrying in case of failure, but this should hopefully help you get started.
Make sure to add a unique constraint or validation as well!
before_validation :set_username
private
def set_username
return if user.blank?
username = "{user}_#{rand(100)}"
end
Another option if you just want a prettier parameter in your url's is to override the to_param method in your user model. This will give you a friendly url like "1-my-name", but when parsed to an integer will just be "1" so rails will still find it when doing lookups.
def to_param
[id, name.parameterize].join("-")
end

Use separate authentication model with Devise on Rails

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.

How can I use validation rules set in a model in several controllers in ruby on rails?

For the sign-up form for my website I don't require that users confirm their passwords so there isn't a password confirmation field.
However when a user resets their password and they click the link in their email and then taken to a change password page I require they confirm their password and provide a field for them to do this.
My question is how can I add password confirmation and min and max lengths for passwords with out it affecting my sign up form because they use they same model. For my signup form I have min and max length validation rules and want these to apply to the change password form but also include password confirmation.
What is the best and most convenient way to do this in your opinion?
If password confirmation should only be done for users already created, the simplest solution is :
class User < ActiveRecord::Base
validates_confirmation_of :password, :unless => :new_record?
end
So it will only call the validation when setting the value for new users and not for users trying to sign up.
You can create your own methods to validate model data, something like this:
class User < ActiveRecord::Base
validate :user_data_validation
def user_data_validation
errors.add(:password_confirmation, "error in the password confirmation") if
!password.blank? and password != password_confirm
end
end
On the other hand you can use control-level validations, but:
Controller-level validations can be tempting to use, but often become
unwieldy and difficult to test and maintain. Whenever possible, it’s a
good idea to keep your controllers skinny, as it will make your
application a pleasure to work with in the long run. (c)

Add a subdomain condition to password reset query in Devise with Rails3?

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

Rails Authlogic Prevent User from Changing their Login/Username

I have implemented Authlogic. I believe that this isn't an authlogic specific quesetion. Assume that I have a User model and each User has a column in the database called "login".
Upon creating a user, the login column is populated. However, I don't want the user to be able to change their login once they set it.
Currently, I have removed the text field in the _form.html.erb file in my views for users. However, it can probably still be accessed through the url right?
How can I make it so that once a login is set, it can not be changed at all?
As far as I know, Authlogic handles this case for you so you don't have to worry about someone overwriting their username. To handle that type of case in general, you can set the username as a protected attribute by excluding it from the attr_accessible attributes in the model, and assigning it directly in the UserController's create action, which gets around the mass assignment protection in the single case where you do want to set it. For example:
class User < ActiveRecord::Base
attr_accessible :email, :password, :other_field1, :other_field2 # note username is not listed
end
class UserController < AppController
def create
#user = User.new(params[:user])
#user.username = params[:user][:username]
#user.save
# ...
end
end

Resources