diff --git a/app/controllers/api/payzen_controller.rb b/app/controllers/api/payzen_controller.rb index 4216e6f88..6b4de2bd0 100644 --- a/app/controllers/api/payzen_controller.rb +++ b/app/controllers/api/payzen_controller.rb @@ -28,32 +28,36 @@ class API::PayzenController < API::PaymentsController @result end + def check_hash + @result = PayZen::Helper.check_hash(params[:algorithm], params[:hash_key], params[:hash], params[:data]) + end + def confirm_payment render(json: { error: 'Bad gateway or online payment is disabled' }, status: :bad_gateway) and return unless PayZen::Helper.enabled? client = PayZen::Order.new - order = client.get(params[:order_id]) + order = client.get(params[:order_id], operation_type: 'DEBIT') amount = card_amount - if order[:transactions].first.status == 'PAID' + if order['answer']['transactions'].first['status'] == 'PAID' if params[:cart_items][:reservation] - res = on_reservation_success(intent, amount[:details]) + res = on_reservation_success(params[:order_id], amount[:details]) elsif params[:cart_items][:subscription] - res = on_subscription_success(intent, amount[:details]) + res = on_subscription_success(params[:order_id], amount[:details]) end end - render generate_payment_response(intent, res) + render res rescue StandardError => e render json: e, status: :unprocessable_entity end private - def on_reservation_success(intent, details) + def on_reservation_success(order_id, details) @reservation = Reservation.new(reservation_params) - payment_method = params[:cart_items][:reservation][:payment_method] || 'stripe' + payment_method = params[:cart_items][:reservation][:payment_method] || 'payzen' user_id = if current_user.admin? || current_user.manager? params[:cart_items][:reservation][:user_id] else @@ -62,17 +66,9 @@ class API::PayzenController < API::PaymentsController is_reserve = Reservations::Reserve.new(user_id, current_user.invoicing_profile.id) .pay_and_save(@reservation, payment_details: details, - intent_id: intent.id, + intent_id: order_id, # TODO: change to gateway_id schedule: params[:cart_items][:reservation][:payment_schedule], payment_method: payment_method) - if intent.class == Stripe::PaymentIntent - Stripe::PaymentIntent.update( - intent.id, - { description: "Invoice reference: #{@reservation.invoice.reference}" }, - { api_key: Setting.get('stripe_secret_key') } - ) - end - if is_reserve SubscriptionExtensionAfterReservation.new(@reservation).extend_subscription_if_eligible @@ -82,7 +78,7 @@ class API::PayzenController < API::PaymentsController end end - def on_subscription_success(intent, details) + def on_subscription_success(order_id, details) @subscription = Subscription.new(subscription_params) user_id = if current_user.admin? || current_user.manager? params[:cart_items][:subscription][:user_id] @@ -92,16 +88,9 @@ class API::PayzenController < API::PaymentsController is_subscribe = Subscriptions::Subscribe.new(current_user.invoicing_profile.id, user_id) .pay_and_save(@subscription, payment_details: details, - intent_id: intent.id, + intent_id: order_id, # TODO: change to gateway_id schedule: params[:cart_items][:subscription][:payment_schedule], - payment_method: 'stripe') - if intent.class == Stripe::PaymentIntent - Stripe::PaymentIntent.update( - intent.id, - { description: "Invoice reference: #{@subscription.invoices.first.reference}" }, - { api_key: Setting.get('stripe_secret_key') } - ) - end + payment_method: 'payzen') if is_subscribe { template: 'api/subscriptions/show', status: :created, location: @subscription } diff --git a/app/frontend/src/javascript/api/payzen.ts b/app/frontend/src/javascript/api/payzen.ts index 379394aa4..8b5a08ae1 100644 --- a/app/frontend/src/javascript/api/payzen.ts +++ b/app/frontend/src/javascript/api/payzen.ts @@ -2,7 +2,7 @@ import apiClient from './clients/api-client'; import { AxiosResponse } from 'axios'; import { CartItems } from '../models/payment'; import { User } from '../models/user'; -import { ConfirmPaymentResponse, CreatePaymentResponse, SdkTestResponse } from '../models/payzen'; +import { CheckHashResponse, ConfirmPaymentResponse, CreatePaymentResponse, SdkTestResponse } from '../models/payzen'; export default class PayzenAPI { @@ -16,6 +16,11 @@ export default class PayzenAPI { return res?.data; } + static async checkHash(algorithm: string, hashKey: string, hash: string, data: string): Promise { + const res: AxiosResponse = await apiClient.post('/api/payzen/check_hash', { algorithm, hash_key: hashKey, hash, data }); + return res?.data; + } + static async confirm(orderId: string, cartItems: CartItems): Promise { const res: AxiosResponse = await apiClient.post('/api/payzen/confirm_payment', { cart_items: cartItems, order_id: orderId }); return res?.data; diff --git a/app/frontend/src/javascript/components/payment/payzen/payzen-form.tsx b/app/frontend/src/javascript/components/payment/payzen/payzen-form.tsx index 544cb99c0..fa19bd43a 100644 --- a/app/frontend/src/javascript/components/payment/payzen/payzen-form.tsx +++ b/app/frontend/src/javascript/components/payment/payzen/payzen-form.tsx @@ -30,14 +30,13 @@ export const PayzenForm: React.FC = ({ onSubmit, onSuccess, onE const { t } = useTranslation('shared'); const PayZenKR = useRef(null); const [loadingClass, setLoadingClass] = useState<'hidden' | 'loader' | 'loader-overlay'>('loader'); - const [hmacKey, setHmacKey] = useState(null); useEffect(() => { const api = new SettingAPI(); - api.query([SettingName.PayZenEndpoint, SettingName.PayZenPublicKey, SettingName.PayZenHmacKey]).then(settings => { - setHmacKey(settings.get(SettingName.PayZenHmacKey)); + api.query([SettingName.PayZenEndpoint, SettingName.PayZenPublicKey]).then(settings => { PayzenAPI.chargeCreatePayment(cartItems, customer).then(formToken => { - KRGlue.loadLibrary(settings.get(SettingName.PayZenEndpoint), settings.get(SettingName.PayZenPublicKey)) /* Load the remote library */ + // Load the remote library + KRGlue.loadLibrary(settings.get(SettingName.PayZenEndpoint), settings.get(SettingName.PayZenPublicKey)) .then(({ KR }) => KR.setFormConfig({ formToken: formToken.formToken, @@ -54,27 +53,31 @@ export const PayzenForm: React.FC = ({ onSubmit, onSuccess, onE /** * Callback triggered on PayZen successful payments + * @see https://docs.lyra.com/fr/rest/V4.0/javascript/features/reference.html#kronsubmit */ const onPaid = (event: ProcessPaymentAnswer): boolean => { - // TODO check hash + PayzenAPI.checkHash(event.hashAlgorithm, event.hashKey, event.hash, event.rawClientAnswer).then(async (hash) => { + if (hash.validity) { + const transaction = event.clientAnswer.transactions[0]; - const transaction = event.clientAnswer.transactions[0]; - - if (event.clientAnswer.orderStatus === 'PAID') { - PayzenAPI.confirm(event.clientAnswer.orderDetails.orderId, cartItems).then(() => { - PayZenKR.current.removeForms().then(() => { - onSuccess(event.clientAnswer); - }); - }) - } else { - const error = `${transaction?.errorMessage}. ${transaction?.detailedErrorMessage || ''}`; - onError(error || event.clientAnswer.orderStatus); - } + if (event.clientAnswer.orderStatus === 'PAID') { + PayzenAPI.confirm(event.clientAnswer.orderDetails.orderId, cartItems).then(() => { + PayZenKR.current.removeForms().then(() => { + onSuccess(event.clientAnswer); + }); + }) + } else { + const error = `${transaction?.errorMessage}. ${transaction?.detailedErrorMessage || ''}`; + onError(error || event.clientAnswer.orderStatus); + } + } + }) return true; }; /** * Callback triggered when the PayZen form was entirely loaded and displayed + * @see https://docs.lyra.com/fr/rest/V4.0/javascript/features/reference.html#%C3%89v%C3%A9nements */ const handleFormReady = () => { setLoadingClass('hidden'); @@ -82,6 +85,7 @@ export const PayzenForm: React.FC = ({ onSubmit, onSuccess, onE /** * Callback triggered when the PayZen form has started to show up but is not entirely loaded + * @see https://docs.lyra.com/fr/rest/V4.0/javascript/features/reference.html#%C3%89v%C3%A9nements */ const handleFormCreated = () => { setLoadingClass('loader-overlay'); @@ -89,6 +93,7 @@ export const PayzenForm: React.FC = ({ onSubmit, onSuccess, onE /** * Callback triggered when the PayZen payment was refused + * @see https://docs.lyra.com/fr/rest/V4.0/javascript/features/reference.html#kronerror */ const handleError = (answer: KryptonError) => { const message = `${answer.errorMessage}. ${answer.detailedErrorMessage ? answer.detailedErrorMessage : ''}`; @@ -115,40 +120,6 @@ export const PayzenForm: React.FC = ({ onSubmit, onSuccess, onE } } - - const checkHash = (answer: ProcessPaymentAnswer, key: string = hmacKey): boolean => { - /* - TODO: convert the following to JS - - ## Check Kr-Answser object signature - def check_hash(answer, key = nil) - supported_hash_algorithm = ['sha256_hmac'] - - # check if the hash algorithm is supported - unless supported_hash_algorithm.include? answer[:hashAlgorithm] - raise PayzenError("hash algorithm not supported: #{answer[:hashAlgorithm]}. Update your SDK") - end - - # if key is not defined, we use kr-hash-key parameter to choose it - if key.nil? - if answer[:hashKey] == 'sha256_hmac' - key = Setting.get('payzen_hmac') - elsif answer[:hashKey] == 'password' - key = Setting.get('payzen_password') - else - raise PayzenError('invalid hash-key parameter') - end - end - - hash = OpenSSL::HMAC.hexdigest('SHA256', key, answer[:rawClientAnswer]) - - # return true if calculated hash and sent hash are the same - hash == answer[:hash] - end - */ - return true; - } - const Loader: FunctionComponent = () => { return (
diff --git a/app/frontend/src/javascript/models/payzen.ts b/app/frontend/src/javascript/models/payzen.ts index c853e2573..4d2ac8990 100644 --- a/app/frontend/src/javascript/models/payzen.ts +++ b/app/frontend/src/javascript/models/payzen.ts @@ -11,6 +11,10 @@ export interface ConfirmPaymentResponse { todo?: any } +export interface CheckHashResponse { + validity: boolean +} + export interface OrderDetails { mode?: 'TEST' | 'PRODUCTION', orderCurrency?: string, diff --git a/app/views/api/payzen/check_hash.json.jbuilder b/app/views/api/payzen/check_hash.json.jbuilder new file mode 100644 index 000000000..e9a363c9c --- /dev/null +++ b/app/views/api/payzen/check_hash.json.jbuilder @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +json.validity !!@result # rubocop:disable Style/DoubleNegation diff --git a/config/routes.rb b/config/routes.rb index a31dde1c9..ec316ccbe 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -183,6 +183,7 @@ Rails.application.routes.draw do post 'payzen/sdk_test' => 'payzen#sdk_test' post 'payzen/create_payment' => 'payzen#create_payment' post 'payzen/confirm_payment' => 'payzen#confirm_payment' + post 'payzen/check_hash' => 'payzen#check_hash' # FabAnalytics get 'analytics/data' => 'analytics#data' diff --git a/lib/pay_zen/helper.rb b/lib/pay_zen/helper.rb index 65c1e6509..0f717db72 100644 --- a/lib/pay_zen/helper.rb +++ b/lib/pay_zen/helper.rb @@ -23,7 +23,8 @@ class PayZen::Helper require 'sha3' content = { cart_items: cart_items, customer: customer }.to_json + DateTime.current.to_s - SHA3::Digest.hexdigest(:sha256, content)[0...12] + # It's safe to truncate a hash. See https://crypto.stackexchange.com/questions/74646/sha3-255-one-bit-less + SHA3::Digest.hexdigest(:sha224, content)[0...24] end ## Generate a hash map compatible with PayZen 'V4/Customer/Customer' @@ -50,5 +51,29 @@ class PayZen::Helper } } end + + ## Check the PayZen signature for integrity + def check_hash(algorithm, hash_key, hash_proof, data, key = nil) + supported_hash_algorithm = ['sha256_hmac'] + + # check if the hash algorithm is supported + raise PayzenError("hash algorithm not supported: #{algorithm}. Update your SDK") unless supported_hash_algorithm.include? algorithm + + # if key is not defined, we use kr-hash-key parameter to choose it + if key.nil? + if hash_key == 'sha256_hmac' + key = Setting.get('payzen_hmac') + elsif hash_key == 'password' + key = Setting.get('payzen_password') + else + raise PayzenError('invalid hash-key parameter') + end + end + + hash = OpenSSL::HMAC.hexdigest('SHA256', key, data) + + # return true if calculated hash and sent hash are the same + hash == hash_proof + end end end diff --git a/lib/pay_zen/order.rb b/lib/pay_zen/order.rb index 80db36f7e..a07afb91f 100644 --- a/lib/pay_zen/order.rb +++ b/lib/pay_zen/order.rb @@ -12,7 +12,7 @@ class PayZen::Order < PayZen::Client # @see https://payzen.io/en-EN/rest/V4.0/api/playground/Order/Get/ ## def get(order_id, operation_type: nil) - post('/Transaction/Get/', orderId: order_id, operationType: operation_type) + post('/Order/Get/', orderId: order_id, operationType: operation_type) end end