3

I'm trying to integrate the new Stripe SCA checkout in my ruby on rails application. I followed the instructions bnwpro wrote here to get started. At this point I get redirected to the Stripe payment page and back to my application after payment is complete.

The issue i'm struggling with is the webhooks, when looking at the Stripe CLI i keep getting an 401...

2020-03-15 17:22:47   --> charge.succeeded [evt_1GMzEgI1EtCroaCdHmRufFBK]
2020-03-15 17:22:47   --> payment_intent.succeeded [evt_1GMzEgI1EtCroaCdWzbnaczh]
2020-03-15 17:22:47  <--  [401] POST http://localhost:3000/stripe-webhooks [evt_1GMzEgI1EtCroaCdHmRufFBK]
2020-03-15 17:22:47  <--  [401] POST http://localhost:3000/stripe-webhooks [evt_1GMzEgI1EtCroaCdWzbnaczh]
2020-03-15 17:22:47   --> payment_method.attached [evt_1GMzEgI1EtCroaCd4quSj3kR]
2020-03-15 17:22:47  <--  [401] POST http://localhost:3000/stripe-webhooks [evt_1GMzEgI1EtCroaCd4quSj3kR]
2020-03-15 17:22:47   --> customer.created [evt_1GMzEgI1EtCroaCdHpEmU1Ai]
2020-03-15 17:22:47  <--  [401] POST http://localhost:3000/stripe-webhooks [evt_1GMzEgI1EtCroaCdHpEmU1Ai]
2020-03-15 17:22:47   --> checkout.session.completed [evt_1GMzEhI1EtCroaCdf1x9gUUR]
2020-03-15 17:22:47  <--  [401] POST http://localhost:3000/stripe-webhooks [evt_1GMzEhI1EtCroaCdf1x9gUUR]

My log shows me:

Started POST "/stripe-webhooks" for 127.0.0.1 at 2020-03-15 17:22:24 +0100
Processing by CheckoutsController#stripe_webhook as XML
  Parameters: {"id"=>"evt_1GMzEJI1EtCroaCdrKXWf4gV", "object"=>"event", "api_version"=>"2018-02-28", "created"=>1584289343, "data"=>{"object"=>{"id"=>"pi_1GMzEJI1EtCroaCdY3wKhKNG", "object"=>"payment_intent", "allowed_source_types"=>["card"], "amount"=>15000, "amount_capturable"=>0, "amount_received"=>0, "application"=>nil, "application_fee_amount"=>nil, "canceled_at"=>nil, "cancellation_reason"=>nil, "capture_method"=>"automatic", "charges"=>{"object"=>"list", "data"=>[], "has_more"=>false, "total_count"=>0, "url"=>"/v1/charges?payment_intent=pi_1GMzEJI1EtCroaCdY3wKhKNG"}, "client_secret"=>"pi_1GMzEJI1EtCroaCdY3wKhKNG_secret_6djH08QObem66sqpCYdB8P9A4", "confirmation_method"=>"automatic", "created"=>1584289343, "currency"=>"usd", "customer"=>nil, "description"=>nil, "invoice"=>nil, "last_payment_error"=>nil, "livemode"=>false, "metadata"=>{}, "next_action"=>nil, "next_source_action"=>nil, "on_behalf_of"=>nil, "payment_method"=>nil, "payment_method_options"=>{"card"=>{"installments"=>nil, "request_three_d_secure"=>"automatic"}}, "payment_method_types"=>["card"], "receipt_email"=>nil, "review"=>nil, "setup_future_usage"=>nil, "shipping"=>nil, "source"=>nil, "statement_descriptor"=>nil, "statement_descriptor_suffix"=>nil, "status"=>"requires_source", "transfer_data"=>nil, "transfer_group"=>nil}}, "livemode"=>false, "pending_webhooks"=>2, "request"=>{"id"=>"req_iRi1XVG06fMcld", "idempotency_key"=>nil}, "type"=>"payment_intent.created", "checkout"=>{"id"=>"evt_1GMzEJI1EtCroaCdrKXWf4gV", "object"=>"event", "api_version"=>"2018-02-28", "created"=>1584289343, "data"=>{"object"=>{"id"=>"pi_1GMzEJI1EtCroaCdY3wKhKNG", "object"=>"payment_intent", "allowed_source_types"=>["card"], "amount"=>15000, "amount_capturable"=>0, "amount_received"=>0, "application"=>nil, "application_fee_amount"=>nil, "canceled_at"=>nil, "cancellation_reason"=>nil, "capture_method"=>"automatic", "charges"=>{"object"=>"list", "data"=>[], "has_more"=>false, "total_count"=>0, "url"=>"/v1/charges?payment_intent=pi_1GMzEJI1EtCroaCdY3wKhKNG"}, "client_secret"=>"pi_1GMzEJI1EtCroaCdY3wKhKNG_secret_6djH08QObem66sqpCYdB8P9A4", "confirmation_method"=>"automatic", "created"=>1584289343, "currency"=>"usd", "customer"=>nil, "description"=>nil, "invoice"=>nil, "last_payment_error"=>nil, "livemode"=>false, "metadata"=>{}, "next_action"=>nil, "next_source_action"=>nil, "on_behalf_of"=>nil, "payment_method"=>nil, "payment_method_options"=>{"card"=>{"installments"=>nil, "request_three_d_secure"=>"automatic"}}, "payment_method_types"=>["card"], "receipt_email"=>nil, "review"=>nil, "setup_future_usage"=>nil, "shipping"=>nil, "source"=>nil, "statement_descriptor"=>nil, "statement_descriptor_suffix"=>nil, "status"=>"requires_source", "transfer_data"=>nil, "transfer_group"=>nil}}, "livemode"=>false, "pending_webhooks"=>2, "request"=>{"id"=>"req_iRi1XVG06fMcld", "idempotency_key"=>nil}, "type"=>"payment_intent.created"}}
Completed 401 Unauthorized in 1ms (ActiveRecord: 0.0ms)

Resulting in the webhoock not being triggerd and the code not being executed.

Who can point me in the right direction? Google and Stack searches have failed me this time...

Edit 1: app > controllers > checkouts_controller.rb

class CheckoutsController < ApplicationController
  require 'stripe'
  before_action :setplans
  skip_before_action :checkStatus
  protect_from_forgery except: :stripe_webhook


  Stripe.api_key = 'sk_test_...'
  endpoint_secret = "whsec_..."



  def index
  end

  def new
    Stripe.api_key = 'sk_test_...'

    @product = Product.find(params[:id])

    session = Stripe::Checkout::Session.create(
      payment_method_types: ['card'],
      line_items: [{
        name: @product.title,
        amount: @product.price,
        currency: 'usd',
        quantity: 1,
      }],
      "metadata": {days: "#{@product.days}"},
      success_url: 'http://localhost:3000/success?session_id={CHECKOUT_SESSION_ID}',
      cancel_url: 'http://localhost:3000/cancel',
    )
    @stripe_session = session
  end

  def success
  ### the Stripe {CHECKOUT_SESSION_ID} will be available in params[:session_id]
    if params[:session_id]
      flash[:success] = "Thank you! Your license has been updated!"
    else
      flash[:danger] = "Session expired error..."
      redirect_to checkouts_path
    end
  end

  def cancel
    redirect_to checkouts_path
  end

  def stripe_webhook
    stripe_response = StripeWebhooks.subscription_events(request)
  end


  private

  def setplans
    @licenseplans = Product.where(active: true)
  end


end

The stripe_webhook lives under app > services > stripe_webhooks.rb

class StripeWebhooks
  require 'stripe'
  STRIPE_API_KEY = "sk_test_..."

  def self.subscription_events(request)
    new(request).subscription_lifecycle_events
  end

  def initialize(request)
    @webhook_request = request
  end

  def subscription_lifecycle_events
    authorize_webhook

    case event.type
    when 'customer.created'
      handle_customer_created
    when 'checkout.session.completed'
      handle_checkout_session_completed
    when # etc.
    end
  end

  private

  attr_reader :webhook_request, :event

  def handle_customer_created(event)
    ## custom actions
  end

  def handle_checkout_session_completed(event)
    ## custom actions
  end

  def authorize_webhook
    Stripe.api_key = 'sk_test_...'

    endpoint_secret = "whsec_..."

    payload = webhook_request.body.read
    sig_header = webhook_request.env['HTTP_STRIPE_SIGNATURE']
    @event = nil

    begin
      @event = Stripe::Webhook.construct_event(
        payload, sig_header, endpoint_secret
      )
    rescue JSON::ParserError => e
      puts e.message
    rescue Stripe::SignatureVerificationError => e
      puts e.message
    end
  end
end

config > routes.rb

...
  # Stripe SCA checkout routes
  get 'success', to: 'checkouts#success'
  get 'cancel', to: 'checkouts#cancel'
  resources :checkouts
  post '/stripe-webhooks', to: 'checkouts#stripe_webhook'
...

There are a number of hardcoded keys for the moment which need to be moved and stored in a safer place after I get this auth error fixed

Dharman
  • 30,962
  • 25
  • 85
  • 135
nick_name
  • 51
  • 6
  • That 401 is coming from your local server. Can you share the code for your CheckoutsController? – cjav_dev Mar 15 '20 at 19:47
  • @w1zeman1p updated with the checkouts_controller and stripe_webhooks. Somewhere a key is invalid or i was thinking about the "sig_header = webhook_request.env['HTTP_STRIPE_SIGNATURE']" part? . Thanks in advance! – nick_name Mar 15 '20 at 20:59
  • It looks like your server is expecting authentication for the webhook POST request, which is why it's returning a 401 - so you need to make sure that route doesn't require authentication. – floatingLomas Mar 16 '20 at 02:27
  • @floatingLomas i had already tried with a skip_before_action :authenticate_user! only for the webhook... (This returned a 500) I'm using Devise in my application – nick_name Mar 16 '20 at 06:55
  • Can you share the 500 error when you skip the before action for authenticate_user! ? I think @floatingLomas is right, here. We need to skip both CSRF and any default authentication that's happening. – cjav_dev Mar 16 '20 at 14:58
  • Thank you both for your help and pointing me in the right direction... I've put my updated controller below which returns all 200's now ! :) – nick_name Mar 16 '20 at 22:24

1 Answers1

2

I've moved the webhook from the service to the controller for the moment and added the skip_before_filter again BUT moved the endpoint_secret key to the webhook and get beautiful 200's now!

Now I just need to extract the metadata part and get working on the appropriate action after a successful payment...

The updated controller:

class CheckoutsController < ApplicationController
  require 'stripe'
  before_action :setplans
  skip_before_action :checkStatus
  protect_from_forgery except: :stripe_webhook
  skip_before_action :authenticate_user!, only: [:stripe_webhook]


  Stripe.api_key = 'sk_test_...'


  def index
  end

  def new
    Stripe.api_key = 'sk_test_...'

    @product = Product.find(params[:id])

    session = Stripe::Checkout::Session.create(
      payment_method_types: ['card'],
      line_items: [{
        name: @product.title,
        amount: @product.price,
        currency: 'usd',
        quantity: 1,
      }],
      metadata: {days: :"#{@product.days.to_s}"},
      success_url: 'http://localhost:3000/success?session_id={CHECKOUT_SESSION_ID}',
      cancel_url: 'http://localhost:3000/cancel',
    )
    @stripe_session = session
  end

  def success
  ### the Stripe {CHECKOUT_SESSION_ID} will be available in params[:session_id]
    if params[:session_id]
      flash[:success] = "Thank you! Your license has been updated!"
    else
      flash[:danger] = "Session expired error..."
      redirect_to checkouts_path
    end
  end

  def cancel
    redirect_to checkouts_path
  end

  def stripe_webhook
    sig_header = request.env['HTTP_STRIPE_SIGNATURE']
    endpoint_secret = "whsec_..."

    begin
      event = Stripe::Webhook.construct_event(request.body.read, sig_header, endpoint_secret)
    rescue JSON::ParserError
      return head :bad_request
    rescue Stripe::SignatureVerificationError
      return head :bad_request
    end

    webhook_checkout_session_completed(event) if event['type'] == 'checkout.session.completed'

    head :ok
  end


  private

  def setplans
    @licenseplans = Product.where(active: true)
  end

  def webhook_checkout_session_completed(event)
    object = event['data']['object']

  end

end
Dharman
  • 30,962
  • 25
  • 85
  • 135
nick_name
  • 51
  • 6