timestamp = request.headers.fetch('X-Signature-Timestamp')
signature = request.headers.fetch('X-Signature')
payload = request.raw_post
data = "#{timestamp}.#{payload}"
expected = OpenSSL::HMAC.hexdigest('SHA256', ENV.fetch('WEBHOOK_SECRET'), data)
event_id = request.headers.fetch('X-Event-Id')
timestamp = request.headers.fetch('X-Signature-Timestamp').to_i
raise ActionController::BadRequest, 'stale request' if Time.now.to_i - timestamp > 300
raise ActionController::BadRequest, 'replay detected' if WebhookEvent.exists?(external_id: event_id)
import crypto from 'crypto';
interface VerifyOptions {
rawBody: Buffer;
signatureHeader: string | undefined;
secret: string;
require 'sinatra/base'
require 'json'
require_relative 'signature_verifier'
class WebhookApp < Sinatra::Base
configure do
const crypto = require('crypto');
function computeSignature(secret, timestamp, payload) {
return crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${payload}`, 'utf8')
package session
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
import hashlib
import hmac
import time
class SignatureError(Exception):
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
// rawBody: true preserves the exact bytes Stripe signed
const app = await NestFactory.create(AppModule, { rawBody: true });
class DownloadsController < ApplicationController
before_action :authenticate_user!, only: :create
skip_before_action :verify_authenticity_token, only: :show
def create
document = current_user.documents.find(params[:document_id])
<?php
namespace App\Controller;
use App\Message\ProcessStripeEvent;
use App\Webhook\StripeSignatureVerifier;
class User < ApplicationRecord
has_secure_password
RESET_TOKEN_TTL = 30.minutes
def self.reset_verifier
class WebhookSignature
class VerificationError < StandardError; end
TOLERANCE = 300 # seconds
def initialize(payload:, header:, secrets:)