Authentication & Authorization
Remote Authentication with OmniAuth
There have been about a dozen popular methods for authenticating Rails applications over the past five years.
As we learn more about constructing web applications there is a greater emphasis on decoupling components. It makes a lot of sense to depend on an external service for our authentication, then that service can serve this application along with many others.
For this section, it’s easiest to understand the concepts by following along and modifying the sample application as you go.
Follow these Setup Instructions to get going with Blogger
Why OmniAuth?
The best application of this concept is the OmniAuth.
It’s popular because it allows you to use multiple third-party services to authenticate, but it is really a pattern for component-based authentication. You could let your users login with their Twitter account, but you could also build your own centralized OmniAuth provider that authenticates all your company’s apps. Maybe you can use the existing LDAP provider to hook into ActiveDirectory or OpenLDAP, or make use of the Google Apps interface.
Better yet, OmniAuth can handle multiple concurrent strategies, so you can offer users multiple ways to authenticate. Your app is just built against the OmniAuth interface, those external components can come and go.
Getting Started with OmniAuth
The first step is to add the dependency to your Gemfile
:
1
|
|
Then run bundle
from your terminal.
Rack Middleware
OmniAuth runs as a "Rack Middleware" which means it’s not really a part of our app, it’s a thin layer between our app and the client.
Create the Initializer
To instantiate and control the middleware, we need an initializer. You’d create /config/initializers/omniauth.rb
and add the following:
1 2 3 |
|
What is all that? Twitter, like many API-providing services, wants to track who’s using it. They accomplish this by distributing API accounts.
Specifically, they use the OAuth protocol which requires a consumer key and a consumer secret. If you want to build an application using the Twitter API you’ll need to register and get your own credentials
Accessing the Remote Service
You need to restart your server so the initializer is run and the middleware loaded. The default URL pattern is:
1
|
|
Where provider
could be twitter
, facebook
, or any other registered OmniAuth provider. We’ll experiment using Twitter.
In your browser go to http://127.0.0.1:8080/auth/twitter and, after a few seconds, you should see a Twitter login page.
Login to Twitter using any account, then you should see a Routing Error from your application. If you’ve got that, then things are on the right track.
Handling the Callback
The authentication pattern starts with your app redirecting to the third party authenticator, the third party processes the authentication, then it sends the user back to your application at a callback URL. OmniAuth defaults to listening at /auth/twitter/callback
.
You’d handle that callback by adding a route in /config/routes.rb
:
1
|
|
Your router will attempt to call the create
action of the SessionsController
when the callback is triggered.
Creating a Sessions Controller
You can generate a controller at the command line and add a create method like this:
1 2 3 4 5 |
|
Calling render :text
is a good debugging technique to display plain text as the response. Here you’d see the response body data stored under the omniauth.auth
key.
Creating a User Model
Even though we’re using an external service for authentication, we’ll still need to keep track of user objects within our system. Let’s create a model that will be responsible for that data.
As you saw, Twitter gives us a ton of data about the user. What should we store in our database? The minimum expectations for an OmniAuth provider are three things:
provider
- A string name uniquely identifying the provider serviceuid
- An identifying string uniquely identifying the user within that providername
- Some kind of human-meaningful name for the user
Let’s start with just those three in our model. From your terminal:
Terminal
$
|
|
Then update the database with rake db:migrate
.
Creating Actual Users
How you create users might vary depending on the application. For our demonstration, we’ll allow anyone to create an account automatically just by logging in with the third party service.
Hop back to the SessionsController
. The controller should have as little domain logic as possible, so we’ll proxy the User lookup/creation from the controller down to the model like this:
1 2 3 |
|
Now the User
model is responsible for figuring out what to do with that big hash of data from Twitter. Open the model file and add this method:
1 2 3 4 5 6 7 8 |
|
To walk through that step by step…
- Look in the users table for a record with this
provider
anduid
combination. If it’s found, you’ll get it back. If it’s not found, a new record will be created and returned - Compare the user’s
name
and thename
in the auth data. If they’re different, either this is a new user and we want to store the name or they’ve changed their name on the external service and it should be updated here. Then save it. - Either way, return the
user
Now, back to SessionsController
, we need to save the logged in user’s id in the session. And let’s add a redirect action to send them to the articles_path
after login:
1 2 3 4 5 |
|
Now visit /auth/twitter
and you should eventually be redirected to the Articles listing.
UI for Login/Logout
That’s exciting, but now we need links for login/logout that don’t require manually manipulating URLs. Anything like login/logout that you want visible on every page goes in the layout.
Open /app/views/layouts/application.html.erb
and you’ll see the framing for all our view templates. Let’s add in the following:
1 2 3 4 5 6 7 8 |
|
If you refresh your browser that will crash for several reasons.
Accessing the Current User
It’s a convention that Rails authentication systems provide a current_user
method to access the user.
Let’s create that in our ApplicationController
with these steps:
Underneath the
protect_from_forgery
line, add this:ruby helper_method :current_user
Just before the closing
end
of the class, add this:ruby private def current_user @current_user ||= User.find(session[:user_id]) if session[:user_id] end
By defining the current_user
method as private in ApplicationController
, that method will be available to all our controllers because they inherit from ApplicationController
.
In addition, the helper_method
line makes the method available to all our views. Now we can access current_user
from any controller and any view!
Progress Check
Refresh the page in your browser and you’ll move on to the next error:
1
|
|
Convenience Routes
Just because we’re following the REST convention doesn’t mean we can’t also create our own named routes. The view snippet we wrote is attempting to link to login_path
and logout_path
, but our application doesn’t yet know about those routes.
Open /config/routes.rb
and add two custom routes:
1 2 |
|
The first line creates a path named login
which redirects to the static address /auth/twitter
which will be intercepted by the OmniAuth middleware. The second line creates a logout
path which will call the destroy
action of our SessionsController
.
With those in place, refresh your browser and it should load without error.
Implementing Logout
Our login works great, but we can’t logout! When you click the logout link it’s attempting to call the destroy
action of SessionsController
. Let’s implement that.
- Open
SessionsController
- Add a
destroy
method In the method, erase the session with:
ruby session[:user_id] = nil
Redirect to the
root_path
with the notice"Goodbye!"
Define a
root_path
in your router like this:ruby root to: "article#index"
Wrapup
At that point, your login/logout system should be working!
That’s just the beginning with OmniAuth. Now, you could choose to add other providers by adding API keys to the initializer and properly handling the different routes.
You might try out some of these:
- A Devise and OmniAuth powered Single-Sign-On implementation: https://github.com/joshsoftware/sso-devise-omniauth-provider
- RailsCast on combining Devise and OmniAuth: http://asciicasts.com/episodes/236-omniauth-part-2
References
- OmniAuth core API documentation: https://github.com/intridea/omniauth
- OmniAuth wiki: https://github.com/intridea/omniauth/wiki