There are already a few great tutorials out there showing how to use oauth and omniauth strategies to login using various online platforms such as the railscasts login with twitter screencast. Using a similar method today we are going to login using Soundcloud's api.
First we need to create a new rails 4 app. I will be using postgreSQL as my database and also skipping the test suite for now.
$rails new login-with-soundcloud-demo-app -T -d postgresql
So now that we have the app created and ready to go we need a login page and a logged in page. First we enter into the app directory and then we generate a static controller with the home action which we then set as our root.
$cd login-with-soundcloud-demo-app $rails g controller static home
And then in our config/routes.rb file we enter the following:
After we have done that we can create the database.
Now if we fire up the server we should get a blank page with some autogenerated placeholder text.
Next up we get to the interesting part. In order to interact with the soundcloud api we can use the ruby wrapper which can be found here https://github.com/soundcloud/soundcloud-ruby. To do that we stick the following lines in our gemfile and run bundle install in the terminal.
#Soundcloud API wrapper gem 'soundcloud'
Now that we have the bones of our app set we need to create a Soundcloud app for allowing us to interact with their API. To do this we go to http://soundcloud.com/you/apps/new, enter your app name and click on 'Register'. On the next page we need to fill in the website url of our app and also the redirect URI. We will be using
http://localhost:3000/ as our URI for now and a made-up url for our website url field. These can (and will need to be) changed later so don't worry about them now. Finally check the checkbox regarding devloper policies and click 'Save App'.
Back in our rails app we now need to setup some sort of user flow that sends the user to Soundcloud, lets them login (if they're not already logged in), confirm that they want to log in with soundcloud and then be redirected back to our app.
The first thing we need is a User model. In this model we need to store the users soundcloud access token and also in order to speed up our app it's probably a good idea to add an index to this column. We do this in the terminal with the following command:
rails g model User soundcloud_user_id:integer:index soundcloud_access_token:string:index
and then we commit the changes to the database with:
Once the migration has run we can start setting up how we will interact with the API. We need a controller to route everything and it's a good idea to call the controller soundcloud so we know exactly what it deals with as our app grows. In the soundcloud controller we need 3 actions, connect, connected and destroy. So let's go right ahead and create the controller but let's also make sure than no views are created as we don't need them.
$rails g controller Soundcloud connect connected destroy --skip-template-engine
Once the controller is generated navigate to it and insert the following code in connect:
def connect # create client object with app credentials client = Soundcloud.new(:client_id => ENV["SOUNDCLOUD_CLIENT_ID"], :client_secret => ENV["SOUNDCLOUD_CLIENT_SECRET"], :redirect_uri => "http://localhost:3000/soundcloud/oauth-callback", :response_type => 'code') # redirect user to authorize URL redirect_to client.authorize_url(:grant_type => 'authorization_code', :scope => 'non-expiring', :display => 'popup') end
Ok there is a lot of important stuff going on here so let's have a look at it. We create a new instance of the
Soundcloud class and assign it some values but where do we get those values from? If we navigate back to the Soundcloud page of the app we just generated we will find our
Client ID and
Client Secret. As we are super careful about security we have hidden their values in environment variables. To do this on Mac OSX I am using the
envyable gem. You can learn how to use it here: https://github.com/philnash/envyable. Basically I have created a YAML file (called
env.yml in my case but you can call it what you like) that I have then added to my
.gitignore file to ensure it doesn't get published to any public repositories. This way my secrets never get exposed. Let's go ahead and add the envyable gem to our gemfile:
#Envirnoment variable manager $gem 'envyable'
and run bundle install:
Next we need to make our YAML file in the config directory and enter our Soundcloud secrets in it (your secrets will of course be different to the ones I enter here).
#env.yml SOUNDCLOUD_CLIENT_ID: a892399b90a5099247b564e0bba71205 SOUNDCLOUD_CLIENT_SECRET: 4f87403b17abb32670ba3cd43f515046
After doing this we need to restart our rails server for the changes to take effect and then we have to add the following lines to .gitignore (adding the secrets.yml is also a good idea so we should do it now).
Now that we have our environment variables setup let's go back and look at the code in our Soundcloud connect action. We can see that the redirect link is slightly different than the one we enter in the soundcloud app settings dashboard so let's quickly update that (replace the
http://localhost:3000 redirect URI with
http://localhost:3000/soundcloud/oauth-callback. Once we have clicked save in the soundcloud app settings dashboard we can go to our
routes.rb and create the necessary routes for everything to work. Now our routes.rb should look like this:
root 'static#home' get '/soundcloud/connect', :to => 'soundcloud#connect' get 'soundcloud/oauth-callback', to: 'soundcloud#connected' get 'logout', to: 'soundcloud#destroy', as: 'logout'
Back in the connect action in the Soundcloud controller we have one last bit of code to review, namely the redirectto call `redirectto client.authorizeurl(:granttype => 'authorization_code', :scope => 'non-expiring', :display => 'popup')`. This is a method built into the Soundcloud class and we have requested an authorization code that is non-expiring so we don't have to worry about refreshing tokens later on. Finally we want to pass it the popup option but you can leave this off if you like.
Next up we need to create a button on our homepage that links to the souncloud#connect action so that the user has something to click on to login. I have grabbed a 'Connect with Soundcloud' image from their site and have put it in the
/app/assets/images folder. On the Static#home page view file (our root) I have placed the following code which should send our user to Soundcloud to login.
<%= link_to image_tag("soundcloud_connect.png"), soundcloud_connect_path %>
We can now click on the button and we will be redirected through the Soundcloud authorization loop. Click connect on the soundlcoud page and you will sent back to your site with the requisite data in tow.
*At this point you may run into an error to do with SSL certificates. It reads something like this:
OpenSSL::SSL::SSLError in SoundcloudController#connected SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed
This occurs when your SSL certs are out of date. If you have this problem there is an easy fix. Using the
certified gem we can ensure our certs are always up to date. Simply add this line to your gemfile and run bundle install.
#Keep SSL certs up to date gem 'certified'
Then in Terminal:
If you needed to install the certified gem restart your server for the changes to take effect.
If you have successfully gotten this far you will run into an error telling you there is no view template for the soundcloud#connected action because we purposefully left this out. Instead we will use the soundcloud#connected action to build an exchange token from the access token soundcloud gave us, add the user to the database (if it's their first time using our app) and then sign the user in (by creating a session for them). Finally we will redirect them to the homepage with a notice telling them they are logged in. The code to do that looks like this:
def connected # create client object with app credentials client = Soundcloud.new(:client_id => ENV["SOUNDCLOUD_CLIENT_ID"], :client_secret => ENV["SOUNDCLOUD_CLIENT_SECRET"], :redirect_uri => "http://localhost:3000/soundcloud/oauth-callback") # exchange authorization code for access token access_token = client.exchange_token(:code => params[:code]) client = Soundcloud.new(:access_token => access_token["access_token"]) # make an authenticated call soundcloud_user = client.get('/me') unless User.where(:soundcloud_user_id => soundcloud_user["id"]).present? User.create_from_soundcloud(soundcloud_user, access_token) end sign_in_user = User.where(:soundcloud_user_id => soundcloud_user["id"]) #create user sessions session[:user_id] = sign_in_user.first.id redirect_to root_url, notice: "Signed in!" end
You may have noticed that there is an undefined method called
create_from_soundcloud being called in the User model. We need to create that method in the User model. It is what will write the user record to the database. In the
/app/models/user.rb file add the following:
def self.create_from_soundcloud(access_token) create! do |user| user.soundcloud_user_id = soundcloud_user["id"] user.soundcloud_access_token = access_token["access_token"] end end
We haven't setup a way to render the notices yet so let's quickly add the following code to the
application.html.erb view file directly between the opening body tag and the yield call.
<% if notice %> <p class="alert alert-success"><%= notice %></p> <% end %> <% if alert %> <p class="alert alert-danger"><%= alert %></p> <% end %>
We also need a way to persist the session with a
current_user variable which we can do in the
application_controller.rb file. All the code for the whole file should look like this:
class ApplicationController < ActionController::Base # Prevent CSRF attacks by raising an exception. # For APIs, you may want to use :null_session instead. protect_from_forgery with: :exception private def current_user @current_user ||= User.find(session[:user_id]) if session[:user_id] end helper_method :current_user end
Finally it would be nice to render a different view depending on whether the user is successfully logged in or not. To do this we will create 2 partials in the
/views/static folder called
_logged_out.html.erb respectively. We will cut and paste the content of the current static#home view into the partial
_logged_out.html.erb replacing the H1 tag with the text
Logged Out and then we will simply add the line
<h1>Logged In</h1> to the
_logged_in.html.erb partial. Finally we will replace the code in the static
home.html.erb to render one partial if there is no user logged in and the other if the user is logged in. The code for that looks like this:
<% if current_user %> <%= render :partial => 'logged_in' %> <% else %> <%= render :partial => 'logged_out' %> <% end %>
OK so let's try our new login flow. Go to the root url and click the soundcloud connect button. Yay, it works!
What if we want to enable the user to log out? Let's add the code for logout to the soundcloud#destroy action (if we had more login options it would probably make more sense to put it in a sessions controller but this will work for now).
def destroy session[:user_id] = nil redirect_to root_url, notice: "Logged out!" end
We also need to add a line to the
_logged_in.html.erb partial to allow the user to log out:
<p><%= link_to "Logout", logout_path %></p>
Now if we go to our root_url and click the link we should be taken back to the login screen. That's it. Soundcloud connect is up and running and ready for the rest of your app. I would highly recommend to checkout the ruby examples on the Soundcloud API documentation to see how you can interact with it and also the documentation for the Soundcloud gem which has some nice examples too.