Acknowledgement
I would like to express my sincere gratitude to @risafj for writing the Guide to devise_token_auth: Simple Authentication in Rails API.
I have implemented an authentication system for my Rails API by following @risafj's guide, and I would like to add Sign in with Twitter on top of it. I can't find any good tutorial on how to add Twitter authentication using Omniauth (gem) with devise_token_auth. So instead, I decided to make up (hack) my own solution.
Note: Guide is for Linux or MacOS.
User workflow
- You click the "Sign in with Twitter" button
- You get redirected to a consent screen
- You click "Authenticate app"
- The page redirects to the actual application
Authentication workflow
Creating a new Twitter application
To make it possible for us to add Twitter authentication to our website, we need to register a new Twitter application on Twitter. This is a necessary step for getting the key and secret code for all communication between our application and Twitter. This is necessary for our application to confirm our identify to Twitter.
We can create a new Twitter application here by clicking Create Project button.
Next, we need to enter our application details.
After we have registered our application, we will get a screen where we can find the key and the secret of our Twitter application, the tab Keys and Access Tokens. Later we will need to use these keys, so save them in your computer for now.
Once this is done, you can use the left side menu bar to navigate to your project to config User authentication settings.
Pick the v1 API
On the bottom of the page we can find the Callback URI / Redirect URL, and Website URL. Fill them up and press the Save button to update the settings. Our Twitter application is now ready.
Open your Rails project, and create a .env file in the project root (if you don't have one yet). Add the Twitter keys you obtain earlier.
# .env
TWITTER_API_KEY=KEY FORM TWITTER
TWITTER_API_SECRET=KEY FORM TWITTER
TWITTER_API_BEARER_TOKEN=KEY FORM TWITTER
TWITTER_API_CALLBACK=http://localhost:4200/twitter_login_oauth_callback
The TWITTER_API_CALLBACK is pointing to your client app, and need to match with the one we submitted to Twitter earlier.
Add Twitter OAuth helper file
Create a twitter.rb
file inside app/services/Oauth/
and copy and paste the code below to the file.
# app/services/Oauth/twitter.rb
require 'net/http'
module Oauth
class Twitter
def initialize(params)
@callback = params[:callback]
@app_key = params[:app_key]
@app_secret = params[:app_secret]
end
def get_redirect
# our front end will go to the callback url
# and user will need to login from there
# e.g.
# https://api.twitter.com/oauth/authenticate?oauth_token=Bviz-wAAAAAAiEDZAAABdOLQn-s
tokens = get_request_token
oauth_token = tokens["oauth_token"]
oauth_token_secret = tokens["oauth_token_secret"]
callback_url = "https://api.twitter.com/oauth/authenticate?oauth_token=#{oauth_token}"
return {
"oauth_token": oauth_token,
"url": callback_url,
"oauth_token_secret": oauth_token_secret
}
end
def obtain_access_token(oauth_token, oauth_token_secret, oauth_verifier)
tokens = get_access_token(oauth_token, oauth_token_secret, oauth_verifier)
end
private
def get_access_token(oauth_token, oauth_token_secret, oauth_verifier)
method = 'POST'
uri = "https://api.twitter.com/oauth/access_token"
url = URI(uri)
oauth_timestamp = Time.now.getutc.to_i.to_s
oauth_nonce = generate_nonce
oauth_params = {
'oauth_consumer_key' => @app_key, # Your consumer key
'oauth_nonce' => oauth_nonce, # A random string, see below for function
'oauth_signature_method' => 'HMAC-SHA1', # How you'll be signing (see later)
'oauth_timestamp' => oauth_timestamp, # Timestamp
'oauth_version' => '1.0', # oAuth version
'oauth_verifier' => oauth_verifier,
'oauth_token' => oauth_token
}
oauth_params['oauth_callback'] = url_encode(@callback+"\n")
oauth_callback = oauth_params['oauth_callback']
base_string = signature_base_string(method, uri, oauth_params)
oauth_signature = url_encode(sign(@app_secret + '&', base_string))
authorization = "OAuth oauth_callback=\"#{oauth_callback}\", oauth_consumer_key=\"#{@app_key}\", oauth_nonce=\"#{oauth_nonce}\", oauth_signature=\"#{oauth_signature}\", oauth_signature_method=\"HMAC-SHA1\", oauth_timestamp=\"#{oauth_timestamp}\", oauth_token=\"#{oauth_token}\", oauth_verifier=\"#{oauth_verifier}\", oauth_version=\"1.0\""
# authorization = 'OAuth oauth_callback="http%3A%2F%2Flocalhost%3A9000%2Ftwitter_connection%0A", oauth_consumer_key="QJImAUogu5MUalOP2Tv5jRt3X", oauth_nonce="a9900fe68e2573b27a37f10fbad6a755", oauth_signature="Y6y8dg4ENFXorvDPu7kyjrdbVYI%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="1601796648", oauth_token="NPASDwAAAAAAiEDZAAABdOzo3sU", oauth_verifier="KiPMEx5rkceLjH1sCV3LfIVsxko0sBrc%0A", oauth_version="1.0"'
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
request = Net::HTTP::Post.new(url)
request["authorization"] = authorization
response = http.request(request)
parse_response_body(response)
end
def get_request_token
# https://wiki.openstreetmap.org/wiki/OAuth_ruby_examples
# http://www.drcoen.com/2011/12/oauth-1-0-in-ruby-without-a-gem/
# http://www.drcoen.com/2011/12/oauth-with-the-twitter-api-in-ruby-on-rails-without-a-gem/
method = 'POST'
uri = "https://api.twitter.com/oauth/request_token"
url = URI(uri)
oauth_timestamp = Time.now.getutc.to_i.to_s
oauth_nonce = generate_nonce
oauth_params = {
'oauth_consumer_key' => @app_key, # Your consumer key
'oauth_nonce' => oauth_nonce, # A random string, see below for function
'oauth_signature_method' => 'HMAC-SHA1', # How you'll be signing (see later)
'oauth_timestamp' => oauth_timestamp, # Timestamp
'oauth_version' => '1.0' # oAuth version
}
oauth_params['oauth_callback'] = url_encode(@callback+"\n")
base_string = signature_base_string(method, uri, oauth_params)
oauth_signature = url_encode(sign(@app_secret + '&', base_string))
oauth_callback = oauth_params['oauth_callback']
authorization = "OAuth oauth_callback=\"#{oauth_callback}\", oauth_consumer_key=\"#{@app_key}\", oauth_nonce=\"#{oauth_nonce}\", oauth_signature=\"#{oauth_signature}\", oauth_signature_method=\"HMAC-SHA1\", oauth_timestamp=\"#{oauth_timestamp}\", oauth_version=\"1.0\""
puts authorization
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
request = Net::HTTP::Post.new(url)
request["authorization"] = authorization
response = http.request(request)
parse_response_body(response)
end
def url_encode(string)
CGI::escape(string)
end
def signature_base_string(method, uri, params)
encoded_params = params.sort.collect{ |k, v| url_encode("#{k}=#{v}") }.join('%26')
method + '&' + url_encode(uri) + '&' + encoded_params
end
def sign(key, base_string)
# digest = OpenSSL::Digest::Digest.new('sha1')
digest = OpenSSL::Digest::SHA1.new
hmac = OpenSSL::HMAC.digest(digest, key, base_string)
Base64.encode64(hmac).chomp.gsub(/\n/, '')
end
def generate_nonce(size=7)
Base64.encode64(OpenSSL::Random.random_bytes(size)).gsub(/\W/, '')
end
def parse_response_body(response)
ret = {}
body = response.read_body
body.split('&').each do |pair|
key_and_val = pair.split('=')
ret[key_and_val[0]] = key_and_val[1]
end
ret
end
end
end
OAuth step-by-step
Step 1. User clicks the "Sign in with Twitter" button in the client app
Step 2. Redirect to the Twitter consent screen to authenticate our app.
In my case, clicking the button calls the oauth_sessions_controller
's get_login_link
method, which then redirect the page.
Create a new file oauth_sessions_controller.rb
, and put it under your controllers/api/v1
directory
# controllers/api/v1/oauth_sessions_controller.rb
module Api::V1
class OauthSessionsController < ApplicationController
# GET /v1/twitter_sign_in_link
def get_login_link
redirect_to OauthSession.get_sign_in_redirect_link
end
end
end
Add a line in your config/routes.rb
file to setup the /v1/twitter_sign_in_link endpoint
.
# config/routes.rb
scope module: 'api' do
namespace :v1 do
...
get 'twitter_sign_in_link' => 'oauth_sessions#get_login_link'
...
end
end
You might ask. What is OauthSession.get_sign_in_redirect_link
? This is going to return us a redirect URL to the Twitter consent screen.
OauthSession
To implement the get_sign_in_redirect_link
, we will first generate the OauthSession
model.
Execute this from your command line.
rails g model OauthSession
This will do many things, including:
- Create a
OauthSession
model, which stores information such asoauth_token
, andoauth_token_secret
, and a corresponding migration file
Add the following code to the newly generated migration file, which looks something like YYYYMMDDTTTT_create_oauth_sessions.rb
class CreateOauthSessions < ActiveRecord::Migration[6.1]
def change
create_table :oauth_sessions do |t|
t.string :provider
t.string :oauth_token
t.string :oauth_token_secret
t.timestamps
end
end
end
Migrate database by running rails db:migrate
.
Implement the get_sign_in_redirect_link
class method to OauthSession.
# models/oauth_session.rb
class OauthSession < ApplicationRecord
def self.get_sign_in_redirect_link
params = {callback: ENV['TWITTER_API_CALLBACK'], app_key: ENV['TWITTER_API_KEY'], app_secret: ENV['TWITTER_API_SECRET']}
twitter_service = Oauth::Twitter.new(params)
result = twitter_service.get_redirect // {oauth_token, oauth_token_secret, url}
oauth_token = result[:oauth_token]
oauth_token_secret = result[:oauth_token_secret]
# Save the token and secret to the table, so that we can use it later
twitter_auth_session = OauthSession.where("oauth_token = ? and provider = ?", oauth_token, "Twitter").first
unless twitter_auth_session
twitter_auth_session = OauthSession.new()
end
twitter_auth_session.oauth_token = oauth_token
twitter_auth_session.oauth_token_secret = oauth_token_secret
twitter_auth_session.provider = "Twitter"
twitter_auth_session.save
return result[:url]
end
end
A quick explanation of the function above:
- Make an API to request to Twitter to get oauth_token, oauth_token_secret and url to consent screen.
- Save the oauth_token, and oauth_token_secret to the table.
Step 3. The user clicks "Authenticate app" on the consent screen
Step 4. The page redirects to your callback_uri
with oauth_token
and oauth_verifier
.
Example URL to redirect to your callback_uri
:
http://localhost:4200/twitter_login_oauth_callback?oauth_token=NPcudxy0yU5T3tBzho7iCotZ3cnetKwcTIRlX0iwRl0&oauth_verifier=uw7NjWHT6OJ1MpJOXsHfNxoAhPKpgI8BlYDhxEjIBY
Our client app will need to make an HTTP POST request to our Rails API endpoint (/v1/twitter_sign_in
) to pass oauth_token
and oauth_verifier
back to our backend server.
Add a line in your config/routes.rb
file to setup the /v1/twitter_sign_in endpoint
.
# config/routes.rb
scope module: 'api' do
namespace :v1 do
...
post 'twitter_sign_in' => 'twitter_auths#sign_in'
...
end
end
Create a twitter_oauths_controller.rb
and save to /controllers/api/v1/
# twitter_oauths_controller.rb
module Api::V1
class TwitterOauthsController < ApplicationController
# POST /v1/twitter_sign_in
def sign_in
# converting the request token to access token
oauth_token = params[:oauth_token]
oauth_verifier = params[:oauth_verifier]
if oauth_token && oauth_verifier
user = TwitterAuth.sign_in(oauth_token, oauth_verifier)
if user
header = user.create_new_auth_token
response.set_header('access-token', header["access-token"])
response.set_header('token-type', header["token-type"])
response.set_header('client', header["client"])
response.set_header('expiry', header["expiry"])
response.set_header('uid', header["uid"])
render json: {data: user}, status: :ok
else
res = {
"success": false,
"errors": [
"Invalid login credentials. Please try again."
]
}
render json: res, status: :unauthorized
end
else
res = {
"success": false,
"errors": [
"Failed to sign in with Twitter"
]
}
render json: res, status: :unauthorized
end
end
end
end
A quick explanation of the code above:
- If
oauth_token
andoauth_verifier
are available, then we will try to use them to sign in to our application. - If the
user
is available, then usecreate_new_auth_token
method fromdevise_token_auth
to generate the header meta
Step 5. Exchange the oauth token for an user
To implement the TwitterAuth.sign_in(oauth_token, oauth_verifier)
, we will first generate the TwitterAuth
model.
Execute this from your command line.
rails g model TwitterAuth
Add the following code to the newly generated migration file, which looks something like YYYYMMDDTTTT_create_twitter_auths.rb
class CreateTwitterAuths < ActiveRecord::Migration[6.1]
def change
create_table :twitter_auths do |t|
t.string :twitter_user_id
t.string :screen_name
t.references :user, foreign_key: true
t.timestamps
end
end
end
Add the user has one twitter_auth relationship.
- In the
user.rb
, addhas_one :twitter_auth
- In the
twitter_auth.rb
, addbelongs_to :user
Migrate database by running rails db:migrate
.
Implement the sign_in
class method to TwitterAuth.
# /models/twitter_auth.rb
class TwitterAuth < ApplicationRecord
belongs_to :user
def self.sign_in(oauth_token, oauth_verifier)
params = {callback: ENV['TWITTER_API_CALLBACK'], app_key: ENV['TWITTER_API_KEY'], app_secret: ENV['TWITTER_API_SECRET']}
# 1. look up the oauth_token_secret by oauth_token
twitter_auth_session = OauthSession.where("oauth_token = ? and provider = ?", oauth_token, "Twitter").first
if twitter_auth_session
twitter_service = Oauth::Twitter.new(params)
oauth_token_secret = twitter_auth_session.oauth_token_secret
twitter_resp = twitter_service.obtain_access_token(oauth_token, oauth_token_secret, oauth_verifier)
twitter_user_id = twitter_resp["user_id"]
screen_name = twitter_resp["screen_name"]
# Here is the fun part
# 1. we look up if the twitter_user_id exist
# if yes, then it's an existing user
# if no, then it's a new user
# for new user we will create a new account
# for existing user, we will look up the user info, and return the header for auth
twitter_auth = TwitterAuth.find_by_twitter_user_id(twitter_user_id)
if twitter_auth
# return the user basic on the twitter id
user = twitter_auth.user
else
# insert a new twitter_auth and also create a new user account
str = (0...8).map { (65 + rand(26)).chr }.join
password = Digest::SHA256.base64digest "#{twitter_user_id}#{screen_name}#{str}".first(8) # generate a password
user = User.create(email: "#{screen_name}@yourwebsite.com", password: password)
twitter_auth = TwitterAuth.new()
twitter_auth.user_id = user.id
twitter_auth.twitter_user_id = twitter_user_id
twitter_auth.screen_name = screen_name
twitter_auth.save
end
twitter_auth_session.delete # remove the auth session
return user
else
return nil
end
end
end
That's all you need to setup a Sign in with Twitter for your Rails API.
In this post, we handled everything manually without replying on gems like omniauth-twitter
. This is more a "hack" than a solution, it might be easier and better to use the omniauth-twitter
gem. I hope you have enjoyed this tutorial, There is much more to be done, but this should get you started. Thanks for reading!
Top comments (1)
Hi Josh! Thank you for the shoutout ๐