Please let me know if you need more information. I'm building a website with a React frontend and Rails 6, which is in api-only mode. I've added Google login to the frontend using the react-google-login component. Once a user has logged in on the frontend, I send the ID token provided by Google to the server, which validates it using the google-id-token gem. This all works fine, but afterwards, I'd like the user to be able to post to certain routes on the server. From googling, I've found a few different solutions, but they all seem to involve sending cookies back and forth.
Using Rails session, store user ID. If configuration is set up correctly, this is sent to the frontend as an HttpOnly cookie, which (as long as credentials: true) sends back the cookie in each following get/post request.
Using Rails cookies, store user ID or JWT as an HttpOnly cookie. Seems basically the same as above but with extra work?
Using Authorize header and JWT. Not 100% clear on how this works - how does Rails get the JWT to the frontend - also cookies?
I've tried all 3 to varying extents, but spent the most time on the first method. However, I can't seem to get Rails to send the session[:token] (that I set after user authentication using Google's ID token) to the frontend. I'm open to switching to another method or one that I haven't listed. Here is what my code looks like.
# application.rb
module MyBackendApi
class Application < Rails::Application
# ... other configs
config.middleware.use ActionDispatch::Cookies
config.middleware.use ActionDispatch::Session::CookieStore
config.middleware.insert_before 0, Rack::Cors do
allow do
origins 'https://my-frontend-app.netlify.app'
resource '*',
credentials: true,
headers: :any,
methods: [:get, :post]
end
end
config.hosts << "my-backend-api.herokuapp.com"
config.hosts << "localhost"
end
end
I also have include ::ActionController::Cookies in ApplicationController.
Below is how I authenticate the user using google-id-token and set the session hash.
class UsersController < ApplicationController
def signin
validator = GoogleIDToken::Validator.new
begin
payload = validator.check(user_params[:idtoken], ENV['GOOGLE_CLIENT_ID'])
user_id = payload['sub']
user = User.find_by(user_id: user_id)
if user.nil?
user = User.create({
user_id: user_id
})
end
session[:user_id] = user_id
render json: {status: :ok}
rescue GoogleIDToken::ValidationError => e
puts "Cannot validate: #{e}"
render json: {message: 'Bad Request', reason: 'Error validating Google token ID'}, status: :bad_request
end
end
Here is how I check whether a user is logged in or not. The method logged_in_user is used as a before_action method for the appropriate controller actions.
def current_user
user_id = session[:user_id]
if !user_id.nil?
user ||= User.find_by(user_id: user_id)
end
end
def logged_in?
!current_user.nil?
end
def logged_in_user
unless logged_in?
render json: {message: 'Not logged in'}, status: :forbidden
end
end
Now for my frontend code. Below is how I authenticate the user (taken almost directly from the Google docs). Note that /googlesignin routes to users#signin in the backend API.
var xhr = new XMLHttpRequest();
xhr.open('POST', 'https://my-backend-api.herokuapp.com/googlesignin');
xhr.withCredentials = true;
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('idtoken=' + idtoken);
Then, I try to post to a /submit route in the backend API.
let headers = new Headers({
'Content-Type': 'application/json'
});
fetch('https://my-backend-api.herokuapp.com/submit', {
method: 'POST',
mode: 'cors',
headers: headers,
cache: 'no-cache',
redirect: 'follow',
referrer: 'no-referrer',
credentials: 'include',
body: JSON.stringify(this.state.selected),
})
.then(function (response) {
let data = response.json()
console.log(data)
return data;
})
I also tried the following, with axios:
axios.post('https://my-backend-api.herokuapp.com/submit', {
submission: this.state.selected,
}, {
withCredentials: true,
})
.then(response => {
console.log(response)
})
.catch(error => {
console.log(error)
})
As I'm very new to cookies, I'm not sure whether I should be able to see the session[:user_id] cookie in Chrome devtools (application tab) or not. I don't see it, so I assume that the frontend is either not accepting or not receiving the cookie from the backend. I'm not sure though.
Any help is super appreciated, I've been struggling with this issue for a while, with no idea what I could be doing wrong. I have been finding user auth to be a little overwhelming thus far. Again, I'm willing to switch to another method. Please let me know if I should post more information. Thank you!
Update:
One thing that works is to manually set a JWT Access-Token header on Rails, and on the frontend, take that header and put it into a cookie. Then when I send later requests, I stick the cookie into an Authorization header, which I check for and validate in Rails. But, is this safe? Intuitively, it doesn't seem secure?