How to do Twitter authentication with devise_token_auth
Tags: rails, devisetokenauth, twitter, ruby
Published: 2021-12-16 07:09:15

##  Acknowledgement
I would like to express my sincere gratitude to @risafj for writing the [Guide to devise_token_auth: Simple Authentication in Rails API](https://dev.to/risafj/guide-to-devisetokenauth-simple-authentication-in-rails-api-pfj).

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.

[Demo video](https://www.loom.com/share/054fa5f058934ba1947f61a7e53cc06b)

![Sign in with Twitter](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ezpwqrblqrcf1hgrxt3a.png) 

## User workflow

1. You click the "Sign in with Twitter" button
2. You get redirected to a consent screen
3. You click "Authenticate app"
4. The page redirects to the actual application

## Authentication workflow

![Twitter authentication process](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s0706e6xqx2licxyw4fw.png)

## 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](https://developer.twitter.com/en/portal/dashboard) by clicking Create Project button.

![Twitter developer portal](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hu4s3ylpg4u4lujot73h.png)

Next, we need to enter our application details.

![Application detail](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cyibeqbrrdg6qhdrcjl1.png)

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.


![App Keys](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4tg39eghcahevgcskzmr.png)

Once this is done, you can use the left side menu bar to navigate to your project to config User authentication settings.

![App settings](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zo1xiaumiydes0qmy5mz.png)

Pick the v1 API

![Twitter API v1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rr21lmomds70a7o9grnn.png)

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.

![Callback URL](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3gi28098al85bwmn9o8i.png)

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.

![Consent screen](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7x2q39i67o0b5evmlak3.png)
 
### 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 as `oauth_token`, and `oauth_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:

1. Make an API to request to Twitter to get oauth_token, oauth_token_secret and url to consent screen.
2. 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:

1. If `oauth_token` and `oauth_verifier` are available, then we will try to use them to sign in to our application.  
2. If the `user` is available, then use `create_new_auth_token` method from `devise_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.  
1. In the `user.rb`, add `has_one :twitter_auth`
2. In the `twitter_auth.rb`, add `belongs_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!