Grasco

External Customer ID

Why extCustomerId exists and how to generate secure, one-way masked identifiers

External Customer ID

The extCustomerId is the identifier you provide when creating a session. It links a user in your system to their profile picture in ProPicture. Since this ID appears in CDN URLs, API calls, and UI components, it is visible on the client side.

Your internal database IDs (auto-increment integers, UUIDs from your users table, etc.) should never be exposed to the public internet. Leaking them can reveal user counts, enable enumeration attacks, or expose internal system details.

Why use a masked ID?

Instead of passing your real customer ID directly, you create a one-way hashed version of it. This gives you a stable, deterministic identifier that:

  • Cannot be reversed — an attacker who sees the extCustomerId in a CDN URL cannot derive your internal user ID
  • Is consistent — the same input always produces the same hash, so you can compute it anywhere without storing a mapping table
  • Is collision-resistant — different inputs produce different hashes

Because the hash is deterministic, you can compute the extCustomerId on the fly whenever you need it — on your backend when creating a session, or on your frontend when rendering a ProfilePicture component. There is no need to store the hashed value in your database (though you can if you prefer).

Use an HMAC (Hash-based Message Authentication Code) with a secret key. This is stronger than a plain hash because even if an attacker knows your hashing algorithm, they cannot reproduce the output without the secret.

ext-customer-id.ts
import { createHmac } from 'crypto';

const SECRET = process.env.EXT_ID_SECRET!; // a long, random secret

export function toExtCustomerId(userId: string): string {
  return createHmac('sha256', SECRET)
    .update(userId)
    .digest('hex');
}

// Usage
const extCustomerId = toExtCustomerId('usr_a1b2c3d4');
// → "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
ext_customer_id.py
import hmac
import hashlib
import os

SECRET = os.environ["EXT_ID_SECRET"].encode()  # a long, random secret

def to_ext_customer_id(user_id: str) -> str:
    return hmac.new(SECRET, user_id.encode(), hashlib.sha256).hexdigest()

# Usage
ext_customer_id = to_ext_customer_id("usr_a1b2c3d4")
extcustomerid.go
package extid

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "os"
)

var secret = []byte(os.Getenv("EXT_ID_SECRET")) // a long, random secret

func ToExtCustomerID(userID string) string {
    h := hmac.New(sha256.New, secret)
    h.Write([]byte(userID))
    return hex.EncodeToString(h.Sum(nil))
}
ext-customer-id.php
<?php

function toExtCustomerId(string $userId): string {
    $secret = getenv('EXT_ID_SECRET'); // a long, random secret
    return hash_hmac('sha256', $userId, $secret);
}

// Usage
$extCustomerId = toExtCustomerId('usr_a1b2c3d4');

Store the EXT_ID_SECRET alongside your other application secrets (e.g., in your environment variables or secret manager). If you rotate it, all existing extCustomerId values will change — you would need to re-create sessions for your users.

Using it end-to-end

1. Create a session (backend)

server.ts
import { GrascoSDK } from '@grasco/sdk';
import { toExtCustomerId } from './ext-customer-id';

const grasco = new GrascoSDK();
grasco.init({ apiKey: process.env.GRASCO_API_KEY });

app.post('/api/avatar-session', async (req) => {
  const extCustomerId = toExtCustomerId(req.user.id);

  const { apiKey } = await grasco.api.createShortLivedApiKey(extCustomerId, {
    permissions: ['images:generate:create', 'images:generate:read'],
  });
  return { apiKey: apiKey.key };
});

2. Display the profile picture (frontend)

Since the hash function is deterministic, your frontend can compute the same extCustomerId — or you can pass it from your backend alongside other user data.

profile.tsx
<ProfilePicture
  extCustomerId={user.extCustomerId}
  alt={user.name}
  size="lg"
/>
<profile-picture
  ext-customer-id="e3b0c44298fc1c14..."
  alt="Jane Smith"
  size="lg"
></profile-picture>

3. CDN URLs

The same extCustomerId works in CDN URLs too:

https://api.propicture.app/profile-picture/cdn/{extCustomerId}_480_original.webp

Alternative: plain hash (simpler, less secure)

If you don't need the extra security of HMAC, a plain SHA-256 hash also works. The trade-off is that an attacker who guesses your user IDs can verify them by hashing.

simple-hash.ts
import { createHash } from 'crypto';

export function toExtCustomerId(userId: string): string {
  return createHash('sha256').update(userId).digest('hex');
}

Plain hashing without a secret is vulnerable to brute-force if your internal IDs are predictable (e.g., auto-increment integers). Use HMAC whenever possible.

Summary

ApproachSecurityRequires secretRecommended
Pass real ID directlyNoneNoNo
Plain SHA-256 hashModerateNoAcceptable
HMAC-SHA-256StrongYesYes

On this page