> ## Documentation Index
> Fetch the complete documentation index at: https://funnelfox.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Password hashes in profile.updated

> Verify FunnelFox passwords in your own auth system using hashes from the `profile.updated` webhook

When a user updates their password through a password input, FunnelFox sends hashed versions of that password in the `profile.updated` webhook payload.

```json theme={null}
{
  "id": "evt_123456789",
  "type": "profile.updated",
  "data": {
    "id": "pro_123456789",
    "email": "user@example.com",
    "phone_number": "+1234567890",
    "password_hashes": {
      "argon2id": "$argon2id$v=19$m=65536,t=1,p=4$...",
      "bcrypt": "$2a$10$...",
      "pbkdf2_sha256": "$pbkdf2-sha256$i=100000$...",
      "scrypt": "$scrypt$n=32768,r=8,p=1$..."
    }
  }
}
```

Learn more in the [webhook reference](/api-reference/webhook-reference/profileupdated).

## Hash formats

FunnelFox normalizes all passwords to UTF-8 NFC before hashing.

Password hashes are provided in four formats:

* `argon2id` — modern, secure algorithm (PHC format)
* `bcrypt` — widely supported (standard bcrypt format)
* `pbkdf2_sha256` — PBKDF2-HMAC-SHA256 (PHC format)
* `scrypt` — memory-hard algorithm (PHC format)

Each hash contains all the parameters needed for verification. Salts are included in the hash strings, so you don't need to store them separately.

### Argon2ID

PHC format:

`$argon2id$v=19$m=65536,t=3,p=4$<base64-salt>$<base64-hash>`

Parameters:

* Memory: 65536 KiB (64 MB)
* Time cost: 3 iterations
* Parallelism: 4 threads
* Hash length: 32 bytes
* Salt length: 16 bytes

### BCrypt

Standard format:

`$2a$12$<22-char-salt><31-char-hash>`

Parameters:

* Cost factor: 12
* Salt length: 16 bytes (encoded as 22 base64 characters)

### PBKDF2-SHA256

PHC format:

`$pbkdf2-sha256$i=210000$<base64-salt>$<base64-hash>`

Parameters:

* Iterations: 210,000
* Hash algorithm: SHA-256
* Hash length: 32 bytes
* Salt length: 16 bytes

### Scrypt

PHC format:

`$scrypt$n=65536,r=8,p=1$<base64-salt>$<base64-hash>`

Parameters:

* N (CPU/memory cost): 65536
* r (block size): 8
* p (parallelization): 1
* Hash length: 32 bytes
* Salt length: 16 bytes

## Verification

To verify passwords in your system, compare the plaintext password against the hash received from the webhook.

### Dependencies

```bash theme={null}
pip install argon2-cffi bcrypt cryptography
```

### Example

<Tip>
  Always use constant-time comparison functions like `hmac.compare_digest` to compare hashes. This protects against timing attacks.
</Tip>

```python theme={null}
import base64
import hashlib
import hmac
import unicodedata
from argon2.low_level import Type, hash_secret_raw
import bcrypt
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
from cryptography.hazmat.primitives import hashes

def normalize_password(password):
    """Normalize password to UTF-8 NFC."""
    return unicodedata.normalize('NFC', password)

def verify_argon2id(password, hash_string):
    """Verify Argon2ID hash."""
    # Parse: $argon2id$v=19$m=65536,t=1,p=4$salt$hash
    parts = hash_string.split('$')

    # Extract parameters from the hash
    params = {}
    for param in parts[3].split(','):
        key, value = param.split('=')
        params[key] = int(value)

    # Decode salt and hash
    salt = base64.b64decode(parts[4] + '==')
    expected_hash = base64.b64decode(parts[5] + '==')

    # Compute hash
    computed = hash_secret_raw(
        secret=normalize_password(password).encode('utf-8'),
        salt=salt,
        time_cost=params['t'],
        memory_cost=params['m'],
        parallelism=params['p'],
        hash_len=len(expected_hash),
        type=Type.ID
    )

    return hmac.compare_digest(computed, expected_hash)

def verify_bcrypt(password, hash_string):
    """Verify BCrypt hash."""
    return bcrypt.checkpw(
        normalize_password(password).encode('utf-8'),
        hash_string.encode('utf-8')
    )

def verify_pbkdf2(password, hash_string):
    """Verify PBKDF2-SHA256 hash."""
    # Parse: $pbkdf2-sha256$i=210000$salt$hash
    parts = hash_string.split('$')
    iterations = int(parts[2].split('=')[1])
    salt = base64.b64decode(parts[3] + '==')
    expected_hash = base64.b64decode(parts[4] + '==')

    # Compute hash
    kdf = PBKDF2HMAC(
        algorithm=hashes.SHA256(),
        length=len(expected_hash),
        salt=salt,
        iterations=iterations
    )
    computed = kdf.derive(normalize_password(password).encode('utf-8'))

    return hmac.compare_digest(computed, expected_hash)

def verify_scrypt(password, hash_string):
    """Verify Scrypt hash."""
    # Parse: $scrypt$n=32768,r=8,p=1$salt$hash
    parts = hash_string.split('$')

    # Extract parameters
    params = {}
    for param in parts[2].split(','):
        key, value = param.split('=')
        params[key] = int(value)

    salt = base64.b64decode(parts[3] + '==')
    expected_hash = base64.b64decode(parts[4] + '==')

    # Compute hash
    kdf = Scrypt(
        salt=salt,
        length=len(expected_hash),
        n=params['n'],
        r=params['r'],
        p=params['p']
    )
    computed = kdf.derive(normalize_password(password).encode('utf-8'))

    return hmac.compare_digest(computed, expected_hash)

# Usage
def verify_password(password, hash_string):
    """Verify password against any supported hash format."""
    if hash_string.startswith('$argon2id$'):
        return verify_argon2id(password, hash_string)
    elif hash_string.startswith('$2a$') or hash_string.startswith('$2b$'):
        return verify_bcrypt(password, hash_string)
    elif hash_string.startswith('$pbkdf2-sha256$'):
        return verify_pbkdf2(password, hash_string)
    elif hash_string.startswith('$scrypt$'):
        return verify_scrypt(password, hash_string)
    else:
        raise ValueError("Unknown hash format")
```

## Troubleshooting

<AccordionGroup>
  <Accordion title="Base64 decoding fails">
    Some Base64 decoders require padding. If decoding fails, add `==` padding as shown in the example code above.
  </Accordion>
</AccordionGroup>
