DEV Community

CHADDA Chakib
CHADDA Chakib

Posted on

Stripe Strong Customer Authentication & Rails

You maybe heard about the migration of the Stripe CB payment to SCA Strong Customer Authentication
All payment from the European countries are concerned.

For more information, The stripe sca doc

The customer experience through the SCA

For your customer, after he puts the Credit card number, the bank can ask for a confirmation by SMS or a push notification in bank app in his smartphone.

Stripe now handle this and for better experience, they need to redirect the user to hosted page by stripe and will handle all the complexity.
When succeed or failed, the user is redirected to your webapp.

This is the flow that explain how this works

Alt Text

The main steps handled by the rails app are two

  1. The preparation of the checkout session [generation of the key]
  2. The validation of the payment [Verify the key]

1. The session preparation

This will prepare Stripe session for the checkout of the product and the redirection when the payment succeed or not.

So we will prepare

  1. The name of the product
  2. The price
  3. The redirection when succeed or not
    secret = gen_secret(@product)
    Stripe::Checkout::Session.create(
      {
        payment_method_types: ['card'],
        line_items: [{
                       name: @product.name,
                       description: @product.description,
                       amount: @product.price,
                       currency: 'eur',
                       quantity: 1,
                     }],
        success_url: confirm_product_payment_url(@product, secret: secret),
        cancel_url: cancel_product_payment_url(@product)
      })


Enter fullscreen mode Exit fullscreen mode

The interesting part here is the gen_secret

We need to generate this secret to be able to be sure and verify that the payment at Stripe succeed and the callback is not faked

We can generate this key like that in rails.
Thanks to rails to provide everything inside the framework and no need to add foreign dependency.

def gen_secret(product)
  key = Rails.application.secrets.secret_key_base
  @crypt = ActiveSupport::MessageEncryptor.new(key[0,32], cipher: 'aes-256-gcm')

  @crypt.encrypt_and_sign(product.token)
end    
Enter fullscreen mode Exit fullscreen mode

As I explained in the previous UML flow, the callbacks containing this secret will be stored at stripe using a secure connection from your server.

The Payment process

The payment process is classic, the customer will enter his credit card numbers and may use a strong authentication if needed (SMS code verification, push notification ... ect)

When the payment succeed

At this step, the stripe servers will send to the browser the redirection to the right callback url.
This callback is containing our secret generated before.
Time to validation !

2. The Validation process

Now, our rails app should verify this callback, in ruby it's easy
For this example I will use a simple verification.

I will only verify if decrypted token is the real token of the product.

  # in the product controller
  def confirm
    @product = current_product

    if PaymentService.new(product: @product)
                     .confirm(secret: params[:secret])
      flash[:success] = "Payment succeed"
    else
      flash[:error] = "Oups Error !"
    end

    redirect_to product_url(@product)
  end

  # inside the PaymentService
    ...
    def confirm(secret)
    if @crypt.decrypt_and_verify(secret) == @product.token
      # handle the success of the payment
      # notifications, emails ...ect

      return true
    else
      return false
    end
  end
Enter fullscreen mode Exit fullscreen mode

The Front-end

Thanks to Stripe the front-end is easy !
So, when the customer want to pay a product, he will click on the payment button.

Alt Text

This will call our rails app to prepare the session, get back the redirection and follow it.

    <%= button_to product_pay_path(@product),
     method: :post,
     id: "pay_sca",
     data: { disable_with: '<i class="fa fa-spinner fa-spin"></i>'.html_safe },
     class: "btn btn-success btn-lg btn-block",
     remote: true do  %>
      <i style="float:left;color:white" id="notes-exists" class="fas fa-lock"></i>
      Payer
    <% end %>
Enter fullscreen mode Exit fullscreen mode

man that's concise !, this will call our controller by ajax, show a cool animation when waiting for the response of the server and finaly follow the redirection to Stripe.

  # controller
  def create
    @company_travel = current_travel.company
    @session_id = PaymentService.new(product: current_product)
                                .create_session.id
  end

Enter fullscreen mode Exit fullscreen mode

As you have maybe notice, we are using a CRUD resource, when a customer need to pay we create a charging resource.

The view part is simple also

# views/.../create.js.erb

const stripe = Stripe('<%= Rails.application.secrets.stripe_public_key %>');


stripe.redirectToCheckout({
  // We put here our session ID
  sessionId: '<%= @session_id %>'
}).then((result) => {
  // If `redirectToCheckout` fails due to a browser or network
  // error, display the localized error message to your customer
  // using `result.error.message`.
});

Enter fullscreen mode Exit fullscreen mode

Easy yay !

I think now we are good to go.

Testing this will be in an other blog post with system tests.

As a gift, The complete service can look like this

class PaymentService
  include Rails.application.routes.url_helpers

  def initialize(product:)
    @product = product

    key = Rails.application.secrets.secret_key_base
    @cryptor = ActiveSupport::MessageEncryptor.new(key[0,32], cipher: 'aes-256-gcm')
  end

  def create_session
    secret = gen_secret

    Stripe::Checkout::Session.create(
      {
        payment_method_types: ['card'],
        line_items: [{
                       name: @product.name
                       description: @product.description,
                       amount: @product.price,
                       currency: 'eur',
                       quantity: 1,
                     }],
        success_url: confirm_product_payment_url(@product, secret: secret),
        cancel_url: cancel_product_payment_url(@product )
      })
  end

  def confirm(secret)
    if @cryptor.decrypt_and_verify(secret) == @product.token
      # handle a succeed payment
      # send notifications, invoices ... ect
      return true
    else
      return false
    end
  end

  private

  def gen_secret
    @cryptor.encrypt_and_sign(@product.token)
  end
end


Enter fullscreen mode Exit fullscreen mode

Latest comments (0)