I have a Rails application that is set to receive a webhook from WooCommerce. Specifically I am looking for when an order is created. I have tested and verified that it works when I have protect_from_forgery except create. Now I am trying to secure my application by verify the webhook. WooCommerce's documentation states the following secret is passed in the request header:
secret: an optional secret key that is used to generate a HMAC-SHA256 hash of the request body so the receiver can verify authenticity of the web hook
WooCommerce github doc
At the moment I am not sure how I am suppose to verify the request, then act on it. And if the request is not authorized reject it with a 401. Here is what I am trying:
class HooksController < ApplicationController
protect_from_forgery
before_action :restrict_access
def order_created_callback
...
end
private
SHARED_SECRET = 'my_secret_key'
def verify_webhook(data, hmac_header)
digest = OpenSSL::Digest::Digest.new('sha256')
calculated_hmac = Base64.encode64(OpenSSL::HMAC.digest(digest, SHARED_SECRET, data)).strip
calculated_hmac == hmac_header
end
def restrict_access
data = request.body.read
verified = verify_webhook(data, env["X-WC-Webhook-Signature"])
head :unauthorized unless verified
end
end
But so far I have been unsuccessful. Any input would be greatly appreciated. Thanks.
Ok I figured out the issue I was having. In case anyone else is trying to work with WooCommerce web hook it seems my issue was appropriately grabbing the header file of the request to match it against my calculated HMAC.
SHARED_SECRET = "my_secret"
def verify_webhook(data, hmac_header)
hash = OpenSSL::Digest::Digest.new('sha256')
calculated_hmac = Base64.encode64(OpenSSL::HMAC.digest(hash, SHARED_SECRET, data)).strip
Rack::Utils.secure_compare(calculated_hmac, hmac_header)
end
def restrict_access
data = request.body.read
head = request.headers["X-WC-Webhook-Signature"]
verified = verify_webhook(data, head)
if verified
return
else
render nothing: true, status: :unauthorized
end
end
Related
I am trying to make my stripe webhook work, using the request.raw_post method instead of the request.body.read method. I am still getting this error:
Signature error
#<Stripe::SignatureVerificationError: No signatures found matching the expected signature for payload>
Also, I am getting only "200" messages in my terminal logs when running stripe trigger checkout.session.completed in my terminal so everything is supposed to work.
This is my Webhooks Controller:
class WebhooksController < ApplicationController
skip_before_action :authenticate_user!
skip_before_action :verify_authenticity_token
# Read the request \body. This is useful for web services that need to
# work with raw requests directly.
def raw_post
unless has_header? "RAW_POST_DATA"
raw_post_body = body
set_header("RAW_POST_DATA", raw_post_body.read(content_length))
raw_post_body.rewind if raw_post_body.respond_to?(:rewind)
end
get_header "RAW_POST_DATA"
end
def create
payload = request.raw_post.read
sig_header = request.env['HTTP_STRIPE_SIGNATURE']
event = nil
begin
event = Stripe::Webhook.construct_event(
payload, sig_header, Rails.application.credentials[:stripe][:webhook]
)
rescue JSON::ParserError => e
status 400
# Invalid payload
puts "Payload error"
return
rescue Stripe::SignatureVerificationError => e
# Invalid signature
puts "Signature error"
p e
return
end
# Handle the event
case event.type
when 'checkout.session.completed'
booking = Booking.find_by(checkout_session_id: event.data.object.id)
booking.update(paid: true)
booking.save
# #booking = Booking.where(checkout_session_id: event.data.object.id)
# #booking.update(paid: true)
# #booking.save
end
render json: { state: "processed" }, status: :ok
end
end
In my webhooks logs on stripe, every event is working except checkout.session.completed where I get a 500 error.
I have been struggling with this error for a while now so I would really appreciate any help :)
I've been battling this for about 24 hours now, and nothing I'm finding in my searches is leading to a solution.
My issue is my session data is not persisting and I can not log in to my app. Everything worked in Dev mode, but has not yet worked in Production. I'm using a Rails 6 Api hosted on Heroku and a React front end. I can successfully make the api call, find the user, and log them in using (I use "puts" to help me log the session at that instance. The session hash has a session_id and user_id at this point):
def login!
session[:user_id] = #user.id
puts "login_session: #{session.to_hash}"
end
After this the app redirects to the user page or an admin page depending on the users authorization.
When the redirect happens that the user or admin page calls the api to see if the user is authorized using:
def logged_in?
puts "logged_in_session: #{session.to_hash}"
!!session[:user_id]
end
The session is empty. Here is my sessions controller:
class SessionsController < ApplicationController
def create
#user = User.find_by(email: session_params[:email])
puts #user.inspect
if #user && #user.authenticate(session_params[:password])
login!
render json: {
logged_in: true,
user: UserSerializer.new(#user)
}
else
render json: {
status: 401,
errors: ['no such user', 'verify credentials and try again or signup']
}
end
end
def is_logged_in?
if logged_in? && current_user
render json: {
logged_in: true,
user: UserSerializer.new(current_user)
}
else
render json: {
logged_in: false,
message: 'no such user or you need to login'
}
end
end
def is_authorized_user?
user = User.find(params[:user_id][:id])
if user == current_user
render json: {
authorized: true
}
else
render json:{
authorized: false
}
end
end
def destroy
logout!
render json: {
status: 200,
logged_out: true
}
end
def omniauth
#user = User.from_omniauth(auth)
#user.save
login!
render json: UserSerializer.new(#user)
end
private
def session_params
params.require(:user).permit(:username, :email, :password)
end
def auth
request.env['omniauth.auth']
end
Would any be able to point me the right direction??
Thank you
I would verify the following:
When first authenticated, does the response from the endpoint include the cookie data?
Check the cookie store in your browser (there's a few extensions you can use to make this easier) and verify that the domain names match and the content in the cookie is what you'd expect.
You can cross reference the cookie ID with the ID in your session store (depending on where you've chosen to store this).
Can you verify the cookie contents (user_id) and session contents in the session store.
Make sure that the cookie data is being sent on the next request after authenticating (check the request headers in the network tab of your dev tools in the browser).
This is all assuming that you're using a browser to talk to this JSON endpoint. APIs usually don't use cookies as it's a browser thing. Alternative authentication mechanisms might be a short lived token (JWT for example) that is generated when authenticating that can be used for subsequent requests.
Quick update: I am able to get the "Set-Cookie: _session_id=..." in the response but it is blocked to due to "SameSite=lax" attribute.
I believe I need to change to SameSite = none, but I'm not sure were to do that.
Any advice?
A bit late but if you're using Rails 6 API, session has been disabled. You need to add the middleware manually. Here is the documentation using-session-middlewares
# This also configures session_options for use below
config.session_store :cookie_store, key: '_interslice_session'
# Required for all session management (regardless of session_store)
config.middleware.use ActionDispatch::Cookies
config.middleware.use config.session_store, config.session_options
I use the gem jwt、devise to build a user login system,
I generate a model Authentication to check the token exist or not.
follow this code:
models/authentication.rb
class Authentication < ApplicationRecord
def self.generate_access_token(email)
payload = {:email => email}
secret = 'secret'
token = JWT.encode payload, secret, 'HS256'
return token
end
end
controllers/user/sessions_controller.rb
def create
user = User.where(email: params[:email]).first
if user&.valid_password?(params[:password])
#token = Authentication.generate_access_token(user.email)
Authentication.create(access_token: #token)
authentications = {token: #token, email: user.email}
render json: authentications, status: :created
else
head(:unauthorized)
end
end
when I do a post request to user/sessions I will get token and user email and store it in localstorage of client, and help me to check the token is valid.
follow this code:
def authenticate_token
token = Authentication.find_by_access_token(params[:token])
head :unauthorized unless token
end
In my question, are there ways to let token don't need to store into database?
You can decode the token and get the email stored in it, and find user by that email.
Suppose you carry the token in the Authorization header, like
Authorization: Bearer <token>
then you can define a before_action to do this:
class ApplicationController < ActionController::API
before_action :authenticate_token
def authenticate_token
token = request.headers['Authorization'].to_s =~ /^Bearer (.*)$/i && $1
return head :unauthorized unless token
payload = JWT.decode(token, 'secret', true, algorithm: 'HS256')
user = User.find_by(email: payload['email'])
return head :unauthorized unless user
# TODO set the `user` as current_user
# How to patch devise's `current_user` helper is another story
end
end
If I were you, I would put user ID in the token, not email, because ID is shorter, and faster to lookup from database, and it exposes nothing personal to the internet (note that JWT is not encrypted. It's just signed).
Or you can skip all these messy things by just using knock instead of devise.
I 'm trying to build a Rails API client. There is an api where I can receive my data as json, which works great so far.
Now I am trying to do some timeout handling but I don't know how. I mean literally. How should I even use timeout handling?
I saw something in a tutorial which I translated for my used gem "net/http" but I cannot imagine that this has even any effect.
Here is my controller code:
require 'net/http'
class OverviewController < ApplicationController
def api_key
ENV["API_KEY"]
end
def handle_timeouts
begin
yield
rescue Net::OpenTimeout, Net::ReadTimeout
{}
end
end
def index
handle_timeouts do
url = "https://example.com/api/#{ api_key }"
uri = URI(url)
response = Net::HTTP.get(uri)
#url_debug = url
#my_hash = response
end
end
end
I'm trying to get my head around the difference of how sessions are handled between GET and POST request.
In rails I'm setting a current_user with a session variable. This works fine for all get requests BUT when I do a POST it seems like the session variable is not carried over. This results in current_user = null
I guess these pictures explains it well.
Cookies on a working GET request - Working get request
Cookies on a NOT working POST request - enter image description here
Why is that?
Do I have to change the header in the angular2 request?
Is it a setting in rails to allow sessions with POST requests.
Here is some of my code...
Angular: Version 1 - Doesn't set my current_user
postSomeData( id : number ){
return this._http.post( "/api/something/" + id,
JSON.stringify("{id: id}") )
.map( response => response.json() )
}
Angular: Version 2 - Doesn't set my current_user
postSomeData( id : number ){
let headers = new Headers();
headers.append('Content-Type', 'application/json');
return this._http.post(
"/api/lists/private/translation/" + id,
JSON.stringify("{id: id}"),
{ headers: headers, withCredentials: true } )
.map( response => response.json() )
}
Rails: ApplicationController
class ApplicationController < ActionController::Base
protect_from_forgery with: :null_session
def current_user
#current_user ||= User.find(session[:user_id]) if session[:user_id]
end
end
Rails 5.0.0.1
Angular 4.0.5
People usually do something like this in the ApplicationController or in a helper:
def set_user
unless #current_user.present?
#current_user = User.where(id: session[:user_id]).take || User.new
end
end
Then you can query if the user has any relations or has an ID, etc...
I faced the very same situation. Turned out that the problem is with the rails server, not angular.
When I make http post request the following statement shows in the logs and my session gets terminated.
WARNING: Can't verify CSRF token authenticity
I searched for this and the discussion on this thread did help me.
Adding the following line in my controller allowed me to make http post reqests.
skip_before_filter :verify_authenticity_token