DEV Community

Ali Zand
Ali Zand

Posted on

Setting Up Google Auth With React, Rails API, and Devise

Problem

When I was trying to setup "Continue with Google" functionality for my personal project I encountered numerous articles and tutorials on how to get it done. Tutorials I found covered setup for a monolith Rails setup or a frontend React setup; however none addressed setup for a Rails API with devise for auth and a React frontend. This is the same project that prompted this tutorial so I will skip the initial setup and focus on the Google OAuth.

For this tutorial we will be taking components from many other articles and tutorials and combining them.

Solution

The solution that I opted for is to have Rails take care of everything instead of having the React frontend receive the OAuth token and pass it back to Rails. So all of the auth setup is done on the backend and the frontend only sends the user to a backend url.

Please note that in my other tutorial I used a non-api setup for Rails but this is also achievable with the API setup. That's the setup I am going to be using for this tutorial.

Steps

  • Set everything up as described here
  • Add SSL to all components (Google requires this) I have my own domain that I used for the server name in the nginx config. To get certs for the dev.my.amazing.website.com I created a temporary nginx server on my staging server with that server name and had letsencrypt create the certs then used scp to copy them over to my local environment. You can also create a wildcard cert if you'd like so you can use it for all environments. Please note that Google does NOT allow self-signed certs

Nginx

server {
  listen 80;
  listen [::]:80;

  server_name dev.my.amazing.website.com;
  return 301 https://dev.my.amazing.website.com;$request_uri;
}

server {
  listen 443 ssl;
  listen [::]:443 ssl;

  ssl_certificate    /etc/nginx/certs/fullchain.pem;
  ssl_certificate_key    /etc/nginx/certs/privkey.pem;

  server_name dev.my.amazing.website.com;

  location / {
    proxy_pass https://web:3000;
  }

  location /api {
    proxy_pass https://app;
  }
}
Enter fullscreen mode Exit fullscreen mode

Please Note: the use of web and app, This is because I am using docker to run my services and that's their name in my compose file.

Rails

To add SSL to the Rails backend we need to update the config/puma.rb to contain the following:

key = "#{File.join('config', 'certs', 'privkey.pem')}"
crt = "#{File.join('config', 'certs', 'fullchain.pem')}"
ssl_bind '0.0.0.0', 443, {
  key: key,
  cert: crt,
  verify_mode: 'none'
}
Enter fullscreen mode Exit fullscreen mode

React

React requires three environment variables to be set in order to enable SSL.

HTTPS=true
SSL_CERT_FILE=path/to/fullchain.pem
SSL_KEY_FILE=path/to/privkey.pem
Enter fullscreen mode Exit fullscreen mode

Things to note:

  • My setup is using docker so the cert files are mounted volumes.
  • Let's encrypt creates symlinks to actual files and docker does not mount symlinks if only the directory is setup as the volume so you'd need to add each symlink as a volume in your compose file.
volumes:
  - "/host/path/to/fullchain.pem:/path/to/fullchain.pem"
  - "/host/path/to/privkey.pem:/path/to/privkey.pem"
Enter fullscreen mode Exit fullscreen mode
  • Install required Gems
# Gemfile
gem "omniauth"
gem "omniauth-linkedin-oauth2"
gem 'omniauth-google-oauth2'
gem 'omniauth-rails_csrf_protection', '~> 1.0' # https://github.com/heartcombo/devise/issues/5236
Enter fullscreen mode Exit fullscreen mode

Then install the gems by using

$ bundle install
Enter fullscreen mode Exit fullscreen mode
  • Route Setup
# routes.rb
scope '/api' do
  devise_for :users,
             controllers: {
               omniauth_callbacks: 'users/omniauth_callbacks',
             }
end
Enter fullscreen mode Exit fullscreen mode
  • Environment Setup
# development.rb and all other environment files
Rails.application.configure do
.
.
.
end

OmniAuth.config.full_host = ENV['FULL_HOST']
Enter fullscreen mode Exit fullscreen mode

Note: This is required so Omniauth gem sends the correct redirect url to the provider.

  • Omniauth Controller
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def google_oauth2
    @user = User.from_google_omniauth(request.env["omniauth.auth"])
    if @user.persisted?
      sign_in_and_redirect @user, event: :authentication
    else
      session["devise.oauth_data"] = request.env["omniauth.auth"].except(:extra) # Removing extra as it can overflow some session stores
      redirect_to '/path/to/frontend/failure/state'
    end
  end

  def failure
    redirect_to '/user/sign_in?error=Unknown'
  end
end
Enter fullscreen mode Exit fullscreen mode
  • user.rb Add these strategies to the devise command
:omniauthable, omniauth_providers: [:google_oauth2]
Enter fullscreen mode Exit fullscreen mode

Add the following functions to the user.rb file

  def self.authenticate(email, password)
    user = User.find_for_authentication(email: email)
    user.try(:valid_password?, password) ? user : nil
  end

  def self.new_with_session(params, session)
    super.tap do |user|
      if session["devise.oauth_data"].present?
        provider = session["devise.oauth_data"]["provider"]
        if provider == "google_oauth2"
          if data = session["devise.oauth_data"]
            user.email = data["info"]["email"] if user.email.blank?
          end
        end
      end
    end
  end

  # first_or_create acts as both signup and signin.
  def self.from_google_omniauth(auth)
    where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
      user.email = auth.extra.id_info.email
      user.password = Devise.friendly_token[0, 20]
    end
  end
Enter fullscreen mode Exit fullscreen mode
  • Add the provider and uid columns to the user model
$ rails g migration AddProviderAndUidToUsersTable
Enter fullscreen mode Exit fullscreen mode

In the migration file:

def change
  add_column(:users, :provider, :string)
  add_column(:users, :uid, :string)
end
Enter fullscreen mode Exit fullscreen mode

And then run the migration using

$ rails db:migrate
Enter fullscreen mode Exit fullscreen mode
  • configure devise
# devise.rb
# Below line is needed since we are not using the monolith 
# rails and cannot send a post request using a button. I 
# tried all sorts of Ajax tricks and even a for submit and it
# rewrote the method as get. This can cause security issues. 
# If you find a solution to this Please leave a comment on 
# this tutorial.
OmniAuth.config.allowed_request_methods = %i[get post]

if Rails.env != 'test'
  if Rails.application.credentials.config.key?(:google)
    config.omniauth :google_oauth2,
                    Rails.application.credentials.dig(:google, :client_id),
                    Rails.application.credentials.dig(:google, :client_secret),
                    {}
  end
end
Enter fullscreen mode Exit fullscreen mode
  • Making sure it the routes are setup
$ rails routes | grep omni 
   user_google_oauth2_omniauth_authorize GET|POST /api/users/auth/google_oauth2(.:format)                                                           users/omniauth_callbacks#passthru
    user_google_oauth2_omniauth_callback GET|POST /api/users/auth/google_oauth2/callback(.:format)                                                  users/omniauth_callbacks#google_oauth2
Enter fullscreen mode Exit fullscreen mode
  • Create App Credentials I followed this documentation from google to obtain the client_id and the client_secret

When setting up the app credentials be sure to enter the redirect URI and the JS Origin to the list. Additionally, since we are not using localhost we need to add test email addresses to the Test users section of the OAuth consent screen. Once the app is ready it can be published to Google for review. If the app is approved then any user can sign in/up via Google without needing to be added to this list.

  • Add the the id and the secret to your credentials file
$ EDITOR=<your_favourite_editor> rails credentials:edit
Enter fullscreen mode Exit fullscreen mode

Please Note That some editors may need the --wait keyword in which case the command look more like this:

$ EDITOR="<your_favourite_editor> --wait" rails credentials:edit
Enter fullscreen mode Exit fullscreen mode

add the following information to your credentials

google:
  client_id: <client_id>
  client_secret: <client_secret>
Enter fullscreen mode Exit fullscreen mode

Conclusion

At this point you can start all your services and if you continue to the passthrue you can successfully Continue with Google.

Top comments (0)