Browse Source

fix: use decoded parameter values for ShopRenter HMAC validation #96

Per ShopRenter documentation, the HMAC is calculated from decoded parameter
values, not URL-encoded ones. This fix uses URLSearchParams which automatically
decodes values, matching the example pseudocode in the docs.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Claude 5 months ago
parent
commit
4e5ad3cf71
1 changed files with 17 additions and 24 deletions
  1. 17 24
      supabase/functions/oauth-shoprenter-callback/index.ts

+ 17 - 24
supabase/functions/oauth-shoprenter-callback/index.ts

@@ -8,36 +8,32 @@ const corsHeaders = {
   'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
 }
 
-// Validate HMAC signature from ShopRenter using raw query string
-function validateHMACFromRawQuery(rawQueryString: string, clientSecret: string): boolean {
+// Validate HMAC signature from ShopRenter
+// Per ShopRenter documentation, HMAC is calculated from decoded parameter values
+function validateHMAC(params: URLSearchParams, clientSecret: string): boolean {
   if (!clientSecret) {
     console.error('[ShopRenter] Client secret is empty or undefined')
     return false
   }
 
-  // Parse query string into key-value pairs preserving URL encoding
-  const params = new Map<string, string>()
-  let hmacValue = ''
-
-  rawQueryString.split('&').forEach(pair => {
-    const [key, ...valueParts] = pair.split('=')
-    const value = valueParts.join('=') // Handle values that contain '='
-    if (key === 'hmac') {
-      hmacValue = value
-    } else {
-      params.set(key, value)
-    }
-  })
-
+  const hmacValue = params.get('hmac')
   if (!hmacValue) {
     console.error('[ShopRenter] HMAC missing from request')
     return false
   }
 
-  // Build sorted query string without HMAC (using URL-encoded values)
-  const sortedParams = Array.from(params.keys())
+  // Build data to validate: all params except hmac, sorted alphabetically
+  const dataToValidate: { [key: string]: string } = {}
+  for (const [key, value] of params.entries()) {
+    if (key !== 'hmac') {
+      dataToValidate[key] = value
+    }
+  }
+
+  // Sort parameters alphabetically by key and create query string
+  const sortedParams = Object.keys(dataToValidate)
     .sort()
-    .map(key => `${key}=${params.get(key)}`)
+    .map(key => `${key}=${dataToValidate[key]}`)
     .join('&')
 
   console.log(`[ShopRenter] HMAC validation - sorted params: ${sortedParams}`)
@@ -170,11 +166,8 @@ serve(wrapHandler('oauth-shoprenter-callback', async (req) => {
       )
     }
 
-    // Validate HMAC using raw query string (preserves URL encoding)
-    const rawQueryString = url.search.slice(1) // Remove leading '?'
-    console.log(`[ShopRenter] Raw query string: ${rawQueryString}`)
-
-    if (!validateHMACFromRawQuery(rawQueryString, shoprenterClientSecret)) {
+    // Validate HMAC using decoded parameter values (per ShopRenter docs)
+    if (!validateHMAC(url.searchParams, shoprenterClientSecret)) {
       return new Response(
         JSON.stringify({ error: 'HMAC validation failed' }),
         { status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }