Setting Up Google Auth With React, Rails API, and Devise
Tags: react, rails, oauth, google
Published: 2024-03-10 03:51:15

## 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](https://dev.to/alizand1992/setting-up-user-auth-with-react-and-rails-minus-the-jwt-headache-39d) 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](https://dev.to/alizand1992/setting-up-user-auth-with-react-and-rails-minus-the-jwt-headache-39d)
* 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](https://letsencrypt.org/getting-started/) create the certs then used [scp](https://man7.org/linux/man-pages/man1/scp.1.html) 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;
  }
}
```
**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'
}
```

### 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
```

### 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"
```

* 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
```
Then install the gems by using
```
$ bundle install
```


* Route Setup
```
# routes.rb
scope '/api' do
  devise_for :users,
             controllers: {
               omniauth_callbacks: 'users/omniauth_callbacks',
             }
end
```

* Environment Setup
```
# development.rb and all other environment files
Rails.application.configure do
.
.
.
end

OmniAuth.config.full_host = ENV['FULL_HOST']
```
**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
```

* user.rb
Add these strategies to the devise command
```
:omniauthable, omniauth_providers: [:google_oauth2]
```

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
```

* Add the provider and uid columns to the user model
```
$ rails g migration AddProviderAndUidToUsersTable
```
In the migration file:
```
def change
  add_column(:users, :provider, :string)
  add_column(:users, :uid, :string)
end
```
And then run the migration using
```
$ rails db:migrate
```

* 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
```

* 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
```

* Create App Credentials
I followed this [documentation](https://developers.google.com/identity/protocols/oauth2) 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
```
**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
```
add the following information to your credentials
```
google:
  client_id: <client_id>
  client_secret: <client_secret>
```

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