Python helpers
The helper functions of the piedap_consent package for webhook signatures, identity proofs, fingerprints, capture hashes, skipped imports and cookies.
piedap_consent exports helper functions next to the client. They check webhook signatures, compute proofs, fingerprints and capture hashes, spot skipped imports, and read the consent cookie.
Requirements#
- Python 3.9 or later.
- For
verify_webhook(): the endpoint's signing secret,whsec_…. - For
identity_proof(): the key's identity secret,idv_…. - For
subject_fingerprint()andcapture_reference_hash(): the project's fingerprint key,sfk_….
Install#
The helpers ship in the same package as the client. See Install.
Import the helpers#
from piedap_consent import (
capture_reference_hash,
identity_proof,
is_skipped_import,
parse_consent_cookie,
parse_partner_cookie,
subject_fingerprint,
verify_webhook,
)The helpers are plain functions. They make no network calls and need no client.
Functions#
verify_webhook()#
Checks that a webhook delivery is genuine and recent.
| Parameter | Type | Required or default | Description |
|---|---|---|---|
payload |
bytes, bytearray, memoryview or str |
Required | The raw request body, before any JSON parsing. |
signature_header |
str or None |
Required | The X-Piedap-Signature header. |
timestamp_header |
str or None |
Required | The X-Piedap-Timestamp header. |
secret |
str or bytes |
Required | The endpoint's signing secret. |
tolerance_seconds |
int |
300 |
How far the timestamp may be from now, in either direction. |
now |
float |
The system clock | The current Unix time. |
Returns bool: True only when the signature matches the raw body and the timestamp is within the tolerance. Missing or malformed headers give False.
Errors Raises ValueError for an empty secret or a negative tolerance. Both are set-up mistakes, not bad deliveries.
import os
from flask import Flask, abort, request
from piedap_consent import (
SIGNATURE_HEADER,
TIMESTAMP_HEADER,
verify_webhook,
)
app = Flask(__name__)
@app.post("/webhooks/piedap")
def consent_hook():
if not verify_webhook(
request.get_data(),
request.headers.get(SIGNATURE_HEADER),
request.headers.get(TIMESTAMP_HEADER),
secret=os.environ["PIEDAP_WEBHOOK_SECRET"],
):
abort(401)
return "", 204In Django, pass request.body as the payload. See Verify signatures.
identity_proof()#
Computes the proof that lets a browser or an app record consent under a signed-in person.
| Parameter | Type | Required or default | Description |
|---|---|---|---|
identity_secret |
str |
Required | The key's identity secret. |
subject_type |
str |
Required | EMAIL, USER_ID or PHONE. |
subject_id |
str |
Required | The identifier, exactly as the page or app sends it. |
Returns str: the lower-case hex HMAC-SHA256 of <subject_type>:<subject_id>.
Errors Raises ValueError for any other subject_type and for an empty identity_secret.
import os
from piedap_consent import identity_proof
proof = identity_proof(
os.environ["PIEDAP_IDENTITY_SECRET"],
"USER_ID",
user.id,
)See Signed-in visitors.
subject_fingerprint()#
Computes the fingerprint that receipts and webhooks carry for a person.
| Parameter | Type | Required or default | Description |
|---|---|---|---|
fingerprint_key |
str |
Required | The project's fingerprint key. |
subject_id |
str |
Required | The identifier you recorded consent under. |
subject_type |
str |
None |
The kind of identifier. Without it, a value that contains @ is treated as an email address. |
Returns str: the lower-case hex HMAC-SHA256 of the identifier. An email address is put in lower case first.
Errors Raises ValueError for an empty fingerprint_key. Here the identifier comes before the type.
import os
from piedap_consent import subject_fingerprint
fingerprint = subject_fingerprint(
os.environ["PIEDAP_FINGERPRINT_KEY"],
"usr_8f2K1xQz",
"USER_ID",
)capture_reference_hash()#
Computes the agent_ref_hash or location_ref_hash that a capture context carries, so you can match it to your own staff id or site code.
| Parameter | Type | Required or default | Description |
|---|---|---|---|
fingerprint_key |
str |
Required | The project's fingerprint key. |
kind |
str |
Required | "agent" for a member of staff, "location" for a branch or site. |
reference |
str |
Required | Your id, as sent in Capture. |
Returns str: the lower-case hex HMAC-SHA256 of <kind>:<reference>, with the reference trimmed and in lower case.
Errors Raises ValueError for another kind, an empty fingerprint_key, or a reference that is empty or not printable ASCII. Raises TypeError when reference is not a string.
import os
from piedap_consent import capture_reference_hash
agent_ref_hash = capture_reference_hash(
os.environ["PIEDAP_FINGERPRINT_KEY"],
"agent",
"EMP-40213",
)See Record consent taken in a branch, by phone or on paper.
is_skipped_import()#
Tells a write with collected_at that the records overrule from a real error.
| Parameter | Type | Required or default | Description |
|---|---|---|---|
error |
BaseException |
Required | What record_consent() raised. |
Returns bool: True for a ConsentError with status 409 and a code in SKIPPED_IMPORT_CODES.
Errors None.
from piedap_consent import ConsentError, is_skipped_import
try:
client.record_consent(**row)
except ConsentError as error:
if not is_skipped_import(error):
raise
skipped.append((row["idempotency_key"], error.code))See Bring in consent given earlier.
parse_consent_cookie()#
Reads the two cookie values the browser runtime wrote.
| Parameter | Type | Required or default | Description |
|---|---|---|---|
value |
str or None |
Required | The piedap_consent cookie value, raw or percent-encoded. |
partners |
str or None |
None |
The piedap_partners cookie value. |
Returns a ConsentCookie, or None.
Errors None. A missing or malformed cookie gives None, so a damaged cookie reads as "no answer".
from piedap_consent import (
COOKIE_NAME,
PARTNER_COOKIE_NAME,
parse_consent_cookie,
)
cookie = parse_consent_cookie(
request.cookies.get(COOKIE_NAME),
request.cookies.get(PARTNER_COOKIE_NAME),
)
analytics = bool(cookie and cookie.granted("analytics"))
matomo = bool(cookie and cookie.granted("analytics", "matomo"))The example reads Flask's request.cookies. In Django, use request.COOKIES.
parse_partner_cookie()#
Reads a piedap_partners value on its own.
| Parameter | Type | Required or default | Description |
|---|---|---|---|
value |
str or None |
Required | The cookie value, raw or percent-encoded. |
Returns dict[str, tuple[str, ...]], purpose key to the partner keys the visitor refused, or None.
Errors None. A missing or malformed cookie gives None.
declined = parse_partner_cookie(request.cookies.get(PARTNER_COOKIE_NAME))
print(declined) # {'analytics': ('matomo', 'plausible')}Types#
ConsentCookie#
A frozen dataclass.
| Field | Type | Description |
|---|---|---|
version |
int |
The format version of the cookie. |
consent_id |
str |
The record's id. It is the text pending for the short time between the visitor's answer and the API confirming it. |
form_version |
int |
The version of the form that was answered. |
decisions |
dict[str, bool] |
Purpose key to allowed. |
decided_at |
datetime |
When the visitor answered, in UTC. |
partners |
dict[str, tuple[str, ...]] |
Purpose key to the partner keys the visitor refused. |
granted(key, partner=None) |
bool |
Whether the purpose is allowed. With partner, whether that partner is allowed too. |
The cookie is not evidence
A visitor controls their own cookies. Use the cookie to decide what to render. Where the answer must be right, read the record with client.get_consent(cookie.consent_id).
The cookie formats are in Cookies and browser storage. The models the client returns are in Models.
Constants#
| Name | Value |
|---|---|
COOKIE_NAME |
piedap_consent |
PARTNER_COOKIE_NAME |
piedap_partners |
SIGNATURE_HEADER |
X-Piedap-Signature |
TIMESTAMP_HEADER |
X-Piedap-Timestamp |
CHECK_TIMEOUT |
1.0, the seconds check_consent() waits unless told otherwise |
MAX_ALSO_KNOWN_AS |
4 |
SKIPPED_IMPORT_CODES |
CONSENT_WITHDRAWN_ON_RECORD, CONSENT_ALREADY_ON_RECORD, CONSENT_NEWER_ON_RECORD, CONSENT_ERASED_ON_RECORD, CONSENT_HELD_UNDER_OTHER_IDENTIFIER |