nick_name
nick_name

Reputation: 51

Stripe SCA webhooks return 401

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

Upvotes: 3

Views: 1152

Answers (1)

nick_name
nick_name

Reputation: 51

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

Upvotes: 2

Related Questions