Ruby on Rails + OmniAuth + Devise: Implementing Custom User Attributes
When starting my first Ruby on Rails application, I knew one thing for certain: instead writing line after line of code to manage user sign up, sessions and authentication, I wanted to take advantage of a great Ruby gem called Devise. For the uninitiated, Devise is billed as a “flexible authentication solution”, but that’s quite an understatement in my opinion. You can check out everything it does here, but to put it simply, Devise handles everything from user registrations, to sessions, to forgotten passwords and everything in between.
Using Devise relieves you of a lot of work and potential headaches and/or bugs. However, if want your User model to have any attributes other than email and password, adjustments will need to be made. Luckily, this is as simple as adding those custom fields to Devise’s pre-built forms! Easy enough.
Devise is a powerful tool on its own, but the project requirements for my app also called for the use of OmniAuth to allow users to sign in or sign up through a third-party like Facebook, Google, or GitHub (I ended up going with Facebook using the OmniAuth-Facebook gem). This is where that hurdle of implementing your additional model attributes into this sign-up flow will be a bit more tricky.
As an example of the issue at hand, let’s say I’m building a job finder Rails app. I have three models: Users, Jobs, and Applications. Users can either sign up as an employer or an applicant (I created an ‘employer’ attribute with a boolean value) — only employers can post jobs, and only applicants can apply to them. With the standard OmniAuth implementation, a user will click a link to direct them to the third-party sign-in form, and once they sign in, they’ll be redirected back to our server in order to create a User instance. What we need to do is get some data from the user before they click that “Sign In with Facebook” link, store that data somewhere, and pass it back to our model upon the User instance being created. So let’s get to it!
NOTE: We are only setting up this user flow for a new registration/sign up, not a new session/log in. This is because a user should only have to specify what type of user they are once — not every time they visit the site.
Step #1 is building out a route and corresponding view and controller action to get that sweet sweet data from our user. In our example, we simply want them to select between signing up as an employer or applicant. Here’s our route for that page — use whatever naming convention you prefer as these routes won’t exactly be following RESTful conventions.
devise_scope :user do
get '/confirm', to: 'users/omniauth_callbacks#landing', as: 'get_landing'
Note that this route will be nested in the :user devise_scope because we are utilizing Devise in our application. Now that we have our route, let’s establish our controller action:
def landing render 'devise/registrations/landing'end
That was easy! Ok, onto our form — for this example, we just need a couple radio buttons and a submit button, plus a POST route and controller action to send the data we receive back to our model. There are a few ways to get this done (and mine probably isn’t the best!), but here’s my form code (including some Bootstrap styling):
<%= form_with url: sending_path do |f| %> <%= f.label :employer, class: "form-check-label" %> <%= radio_button_tag :employer, 1, class: "form-check-input" %> <%= f.label :employer, "Applicant", class: "form-check-label" %> <%= radio_button_tag :employer, 0, class: "form-check-input" %> <br><br> <%= f.submit "Submit", class: "btn btn-outline-primary" %><% end %>
And here’s the sending_path route that is being used as that form’s URL (placed in the same block under devise_scope :user as the previous route):
post '/confirm', to: 'users/omniauth_callbacks#add_to_session', as: 'sending'
Ok, now we’re cooking! This is where the rubber meets the road — we’re going to save that user-provided data into the user’s session (a great place to store small amounts of data needed through multiple requests!). Since my employer attribute is a boolean, I’m going to stick with 1’s and 0’s. As you can see in the form above, selecting ‘employer’ will set a value of 1, and selecting ‘applicant’ will set a value of 0. Here’s how we move that data into our sessions hash in the controller:
def add_to_session if params[:employer] == "1" session[:employer] = 1 else session[:employer] = 0 end redirect_to staging_pathend
Super simple, right? Now we have access to the user’s selection in the session[:employer] hash from any controller. That last line of code is redirecting the user to a landing page which finally has the ‘Sign In with Facebook’ button we’ve been waiting for (for Facebook Oauth, it’s taking the user to /users/auth/facebook). We’re almost there — but if we click that link now it will send the user through the standard Oauth sign-up flow, and we need to first make some adjustments so that our user is created with their user-type selection in mind.
First, let’s add some logic to the #facebook controller action to parse our session[:employer] hash into a boolean value that our model will be able to read and assign to the employer attribute.
employer = falseif session[:employer] == 1 employer = trueend
Next, we’ll update the .from_omniauth method (this should’ve been set-up when first implementing OmniAuth!). My solution was to add a second argument to this class method — in my case, the ‘employer’ variable which houses that boolean value. Once inside that method, set the user’s employer attribute equal to that variable you just passed in, and boom — your user who logged in via Facebook now has a designated user type.
NOTE: If you have OmniAuth set up to not only create but update a user upon signing in, you’ll want to use the fancy ||= operator when setting the user’s employer value. This is so that when a returning user signs back in using Oauth, the existing value of their employer attribute is used instead of a new (and likely nil) value.
The final .from_omniauth method should look something like this, more or less:
def self.from_omniauth(auth, employer) user = find_or_initialize_by(provider: auth.provider, uid: auth.uid) user.email = auth.info.email user.password = Devise.friendly_token[0, 20] user.name = auth.info.name user.employer ||= employer user.image = auth.info.image user.save userend
And there you have it. Now, not only do you have a modern sign-up/log-in system, but it’s customized to your liking. As I was working through this process I wasn’t able to find any blogs documenting how to go about this, so if you encountered the same problem, I hope this helped!