Sending cookie in XHR response but it's not getting stored? - ruby-on-rails

I'm trying to pass an HttpOnly cookie with a response from an API I'm writing. The purpose is for the cookie act like a refresh token for the purpose of silent refresh for a SPA in React.
In my controller method, I've got the following code:
response.set_cookie(
:foo,
{
value: 'testing',
expires: 7.days.from_now,
path: '/api/v1/auth',
secure: true,
httponly: true
}
)
I'm making a post request to this action with a fetch command like so:
fetch("http://localhost:3001/api/v1/auth", {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: 'aaron#example.com',
password: '123456',
})
})
Not sure if that matters but I'm wondering if passing cookies in a XHR response doesn't work? However, this seems to be working as you can see in my response I'm getting this:
Set-Cookie: foo=testing; path=/api/v1/auth; expires=Sun, 26 Jan 2020 05:15:30 GMT; secure; HttpOnly
Also in the Network tab under Cookies I'm getting this:
However, I'm NOT getting the cookie set under Application -> Cookies:
To clarify, the React app is sitting on localhost:3000 and the rails backend is listening on localhost:3001.
Any ideas?

Ok, so it looks like I needed to configure my CORS (in Rails this is your Rack::CORS middleware.
I setup my config/initializers/cors.rb file like so:
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins 'http://localhost:3000'
resource '*',
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options, :head],
credentials: true
end
end
and my fetch command should look something like this with credentials: 'include' as a parameter:
return fetch(`${endPoint}`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email,
password,
password_confirmation: passwordConfirmation
})
})
Adding credentials: true allows cookies to be set by the browser. Apparently, even if you send them, you need Access-Control-Allow-Credentials: true in the headers for the browser to do anything with them.
EDIT: Recreating this application for learning experience I came across this issue again even after including the credentials option. I wasn't seeing the HttpOnly cookie being stored in the browser. Turns out however, that it WAS and does get sent. You can probably test for this in a controller action. Keep this in mind if this solution doesn't 'seem' to work for you!

Related

No Cookies with Rails API, CORS and Next getInitialProps

I have a Rails API only backend with rack-cors installed at port 3000.
My NextJS is running on port 5000.
My goal is to consume a cookie set by the server within my NextJS app. It contains a JWT token which holds an email and roles, which will be decoded and used for Authorization.
With Postman I can see the Cookie.
I have set my Cookie in Rails with httpOnly, but I can't see it my Chrome-dev-tools.
When I set a httpOnly cookie with NextJS, it's present.
I can see it in the DevTools "response" when POST to Rails's /login but not in the Application -> Storage -> Cookies(see images attached).
It seems Chrome does not set the cookie in DevTools "storage", it's always empty. Equally if I use "httpOnly" or not.
Can someone help me to consume the Cookie on NextJS (client side)?
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins 'localhost:5000', '127.0.0.1:5000'
resource '*',
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options, :head],
expose: ['Per-Page', 'Total', 'Link', 'ETag', 'Set-Cookie']
end
Here I set my cookie in the Rails controller:
response.set_cookie(
"withIP",
{
value: auth_token[:token],
expires: 30.minutes.from_now,
path: "/",
domain: "localhost:5000", # tried to remove domain completly also tried "127.0.0.1" as domain, as well as '.localhost'
secure: Rails.env.production?,
httponly: true # Rails.env.production?
}
)
This is my request in the NextJS app:
return fetch(`${PublicApiUrl}/auth/login.json?${qs.stringify(options)}`,
{ method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
credentials: 'same-origin', // also tried 'credentials' and with 'inline', as well completely removed at all.
})
.then((response) => {
console.log("RESPONSE", response); // 1* see image below
console.log("RESPONSE HEADERS", response.headers); // *2 ALWAYS undefined
console.log("RESPONSE forEeach", response.headers.forEach(console.log)); // ALWAYS undefined
console.log("response.headers.get('set-cookie')", response.headers.get('set-cookie')); // NULL
console.log("document.cookie", document.cookie); // nope
return response.json()
} )
.then((data)=>{
console.log("DATA", data); // returns my authToken
return data;
})
*1 console.log from above
RESPONSE
Response {type: 'cors', url: 'http://localhost:3000/auth/login.json?email=xxxxxxx&password=xxxxxx', redirected: false, status: 200, ok: true, …}
body: (...)
bodyUsed: true
headers: Headers
[[Prototype]]: Headers
ok: true
redirected: false
status: 200
statusText: "OK"
type: "cors"
url: "http://localhost:3000/auth/login.json?email=XXXXXX&password=XXXXXX"
*2 RESPONSE HEADERS (console.log from above)
RESPONSE HEADERS Headers {}[[Prototype]]: Headers

Safari won't set cookie from same domain (different subdomain)

I use a Rails backend and React frontend. I recently deployed my app to Heroku using the following naming convention: app-name-frontend.herokuapp.com and app-name-backend.herokuapp.com, so the domain is the same. However, when I send a cookie from the api to frontend, I can see the cookie being sent but its never saved. My cors is setup as follows:
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins 'app-name-frontend.herokuapp.com'
resource '*',
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options, :head],
credentials: true
end
end
And I set my Cookie as follow:
{
value: JsonWebToken.encode(user_id: user.id),
expires: 1.day,
httponly: true,
secure: true,
same_site: :lax,
domain: :all
}
Since I have domain as :all this means that it should work on all domains and subdomains, and since both are on the domain herokuapp.com shouldn't Safari set the cookie? What am I doing wrong?
May be the HttpOnly attribute is added in your cookie settings. If the HttpOnly flag is included in the HTTP response header, the cookie cannot be accessed through the client-side script. You can find a detailed explanation here

Using Rails, how should I configure my application.rb in order to allow for CORS requests to take place?

I am using Rails as a backend API to support a frontend that is made with VueJS. I want to send a POST request from VueJS when a button is pressed on the website, I have used axios to do this
`
methods: {
// Performs a POST request to the rails endpoint and passes the email and pass as parameters
signup() {
if (this.password === this.password_confirmation) {
this.$http.plain
.post("/signup", {
email: this.email,
password: this.password,
password_confirmation: this.password_confirmation
})
// If successful execute signinSuccesful
.then(response => this.signinSuccesful(response))
// If it doesn't run for whatever reason, execute signupFailed
.catch(error => this.signinFailed(error));
}
},`
This should in theory create a POST request that would be received by the API, I have attempted to catch the request like this: post "signup", controller: :signup, action: :create
However, when I look in the console on chrome I get an error when I first load the site:
`0/signin net::ERR_CONNECTION_REFUSED`
And another error when I click the button to send the POST request:
*
Access to XMLHttpRequest at 'http://localhost:3000/signup' from origin
'http://localhost:8080' has been blocked by CORS policy: Request
header field content-type is not allowed by
Access-Control-Allow-Headers in preflight response.
My 'application.rb' file looks like this:
`module RecordstoreBackend
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 6.0
config.api_only = true
config.middleware.insert_before 0, Rack::Cors do
allow do
origins '*'
resource '*', headers: :any, methods: [:get, :patch, :put, :delete, :post, :options]
end
end`
I think the issue is this file, but I am not sure how to configure it. Thanks for your help.
It seems to me like you're missing the host for that request (on your Vue app). Notice the error says 0/signup which indicates that it's sending the request to http://0/signup that is, in turn, "rejecting" the connection.
I'm not familiar with Vue.js structure, but I'd recommend encapsulating the axios call on a plugin that would use the host from an environment configuration file, so that you can have different hosts for localhost and your production env. Or even just add the host to your environment:
methods: {
// Performs a POST request to the rails endpoint and passes the email and pass as parameters
signup() {
if (this.password === this.password_confirmation) {
this.$http.plain
.post(`${process.env.API_HOST}/signup`, {
email: this.email,
password: this.password,
password_confirmation: this.password_confirmation
})
// If successful execute signinSuccesful
.then(response => this.signinSuccesful(response))
// If it doesn't run for whatever reason, execute signupFailed
.catch(error => this.signinFailed(error));
}
},
Reference: https://cli.vuejs.org/guide/mode-and-env.html#environment-variables
If you want to fix it the way you have it now, simply add your host to the axios call:
methods: {
// Performs a POST request to the rails endpoint and passes the email and pass as parameters
signup() {
if (this.password === this.password_confirmation) {
this.$http.plain
.post("http://localhost:3000/signup", {
email: this.email,
password: this.password,
password_confirmation: this.password_confirmation
})
// If successful execute signinSuccesful
.then(response => this.signinSuccesful(response))
// If it doesn't run for whatever reason, execute signupFailed
.catch(error => this.signinFailed(error));
}
},
About the CORS error, check this: https://stackoverflow.com/a/25727411/715444

POST request works in Postman but is blocked from React front end. No CORS error, not making it to the server

I have a database setup on Dokku that is currently functioning fine with Postman. My full stack React front end and Rails back end are functioning locally, but when I attempt the same fetch calls (only difference in code is the url), the requests don't have any parameters and are blocked before ever reaching the server.
Here's an example of a fetch call I'm trying (using Redux):
export const createPurchase = purchase => {
return dispatch => {
return fetch(baseURL + 'purchases', {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accepts": "application/json"
},
body: JSON.stringify({ amount: purchase.amount, bundle_name: purchase.bundleName, user_id: purchase.userId })
})
.then(resp => resp.json())
.then(userObj => {
console.log('purchase created!')
})
.catch(error => console.log(error))
}
}
Reading my Rails server logs on Dokku shows that it isn't aware that requests are being made. My CORS file looks like this:
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins '*'
resource '*',
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options, :head]
end
end
I'm not sure why the fetch requests are automatically getting blocked when I am allowing everything with CORS, do I need to add a header to the fetch requests?

How can I have a Rails API set a JWT in a cookie on a client that is running on another domain?

Long time lurker, first time poster here.
There are many good guides and resources about JWTs and how and where to store them. But I'm running into an impasse when it comes to securely storing and sending a JWT between a ReactJS/Flux app running on a Node server and a completely separate Rails API.
It seems most guides tell you to just store the JWT in local storage and pluck it out for every AJAX request you make and pass it along in a header. https://stormpath.com/blog/where-to-store-your-jwts-cookies-vs-html5-web-storage/ warns against this, however, since local storage is not secure and a malicious person could access that token. It recommends storing it in the cookie instead and just letting the web browser pass it along with each request.
That sounds fine to me since from what I understand cookies get conveniently sent along with every request anyway. It means I can just make AJAX requests from my ReactJS app to my Rails API and have the API pluck it out, check it, and do it's thing.*
The problem I'm running into is my Node application doesn't set a cookie from the response it gets back from the Rails API even though the Rails API (running on localhost:3000) returns a Set-Cookie header and sends it back to the ReactJS/Node app (running on localhost:8080).
Here's my login controller action on my Rails API side:
class V1::SessionsController < ApplicationController
def create
user = User.where(email: params[:user][:email]).first!
if user && user.authenticate(params[:user][:password])
token = issue_new_token_for(user)
# I've tried this too.
# cookies[:access_token] = {
# :value => token,
# :expires => 3.days.from_now,
# :domain => 'https://localhost:8080'
# }
response.headers['Set-Cookie'] = "access_token=#{token}"
render json: { user: { id: user.id, email: user.email }, token: token }, status: 200
else
render json: { errors: 'username or password did not match' }, status: 422
end
end
end
The gist of it is it takes an email and password, looks the user up, and generates JWT if the info checks out.
Here's the AJAX request that is calling it from my Node app:
$.ajax({
url: 'http://localhost:3000/v1/login',
method: 'post',
dataType: 'json',
data: {
user: {
email: data.email,
password: data.password
},
callback: '' //required to get around ajax CORS
},
success: function(response){
console.log(response);
},
error: function(response) {
console.log(response);
}
})
Inspecting the response from the Rails API shows it has a Set-Cookie header with a value of access_token=jwt.token.here
Screenshot:
Chrome Dev Tools Inspector Screenshot
However, localhost:8080 does not show any cookies set and subsequent AJAX calls from my Node/React app do not have any cookies being sent along with them.
My question is, what piece(s) am I misunderstanding. What would I have to do to make storing JWTs in cookies work in this scenario?
A follow-up question: assuming storing the JWT in a cookie is not an option, what potential security risks could there be with storing the JWT in local storage (assuming I don't put any sensitive info in the JWT and they all expire in some arbitrary amount of time)?
*this may be a fundamental misunderstanding I have. Please set me straight if I have this wrong.
Side-notes that may be of interest:
My Rails API has CORS setup to only allow traffic from localhost:8080
in development.
In production, the Node/React app will probably be
running on a main domain (example.com) and the Rails API will be
running on a sub domain (api.example.com), but I haven't gotten that
far yet.
There's nothing sensitive in my JWT, so local storage is an
option, but I want to know why my setup doesn't work with cookies.
Update elithrar submitted an answer that worked:
I needed to modify my AJAX request with xhrFields and crossDomain as well as tell jQuery to support cors:
$.support.cors = true;
$.ajax({
url: 'http://localhost:3000/v1/login',
method: 'post',
dataType: 'json',
xhrFields: {
withCredentials: true
},
crossDomain: true,
data: {
user: {
email: data.email,
password: data.password
}
},
success: function(response){
console.log(response);
},
error: function(response) {
console.log(response);
}
})
And I added credentials: true and expose: true to my Rack Cors configuration on my Rails API (the * is only for my development environment):
config.middleware.insert_before 0, 'Rack::Cors' do
allow do
origins '*'
resource '*', :headers => :any, :methods => [:get, :post, :put, :path, :options], credentials: true, expose: true
end
end

Resources