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 temporarynginx
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;
}
}
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 from google to obtain the
client_id
and theclient_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.
Top comments (0)