Secure Encrypted Token Workflow in CKAN-2.10.4 (Email and Admin level Validation)

Table of contents
- While building a secure access request system in CKAN for internal datasets, I faced a key challenge:
- Why Use Encrypted Tokens (Instead of Raw UUIDs)?
- Step 1: Token Generation with hashlib + base64
- Step 2: Store in Database
- Step 3: Email the Token
- Step 4: Validate the Token
- Benefits of This Approach
- Stack Recap

While building a secure access request system in CKAN for internal datasets, I faced a key challenge:
How do we let users request access securely and validate them via email, without exposing raw tokens or relying on basic UUIDs?
I needed a system that generated one-time, time-bound, tamper-proof tokens for each access request.
Instead of using Fernet or JWT, I implemented a lightweight but secure solution using
hashlib
,base64
, and a shared secret.
Why Use Encrypted Tokens (Instead of Raw UUIDs)?
Raw UUIDs are guessable if exposed in patterns.
They can be tampered with if not verified properly.
Encryption ensures:
The token is unreadable and URL-safe
No one can forge a valid token without the secret
We can expire it on the server without storing sessions
Step 1: Token Generation with hashlib
+ base64
import hashlib
import base64
import uuid
from datetime import datetime, timedelta
SECRET_KEY = "your-secret-key"
def generate_token():
raw_token = str(uuid.uuid4())
hash_object = hashlib.sha256((raw_token + SECRET_KEY).encode())
token = base64.urlsafe_b64encode(hash_object.digest()).decode()
return token, raw_token
We generate a raw_token
using UUID and encrypt it by hashing it with a secret key. The hash is then base64-encoded to make it URL-safe. We store both token
(encrypted) and raw_token
(plaintext for one-time validation).
Step 2: Store in Database
token, raw_token = generate_token()
request = DatasetAccessRequest(
user_id=user_id,
dataset_id=dataset_id,
token=token,
raw_token=raw_token, # used only once for validation
expires_at=datetime.utcnow() + timedelta(hours=24),
status='pending'
)
model.Session.add(request)
model.Session.commit()
We save the encrypted token, raw token, expiry, and status in the DB. The raw token will only be used once when the user clicks the email link.
Step 3: Email the Token
link = f"{h.url_for(controller='access', action='validate', qualified=True)}?token={raw_token}&dataset_id={dataset_id}"
email_body = f"""
Hi {admin['name']},
Click below to approve {new_user}'s request:
{link}
or reject the request:
{link}
The link expires in 24 hours.
"""
The email includes only the raw_token
. We never expose or share the encrypted hash. This keeps the token lightweight and secure.
Step 4: Validate the Token
def validate_token(raw_token, dataset_id):
expected_token = base64.urlsafe_b64encode(
hashlib.sha256((raw_token + SECRET_KEY).encode()).digest()
).decode()
req = model.Session.query(DatasetAccessRequest).filter_by(
dataset_id=dataset_id,
raw_token=raw_token,
token=expected_token,
status='pending'
).first()
if req and req.expires_at > datetime.utcnow():
req.token_validated = True
model.Session.commit()
return True
return False
When the user clicks the link, we recompute the encrypted hash and match it against the stored one. If the record exists and hasn't expired, the request is marked as validated.
Benefits of This Approach
Tokens are tamper-proof and can't be reverse-engineered
Supports expiry (via TTL or timestamp)
Token can be shared securely via email links
Works seamlessly with CKAN’s existing plugin system
Lightweight (no dependency on external encryption libs)
Stack Recap
Python
CKAN
(plugin controller + models)hashlib
,base64
,uuid
PostgreSQL
(CKAN ORM)Email-based access flow
This was part of a larger workflow that eventually adds users to datasets after admin approval. Next up: handling admin-side review + role assignment.
If you've implemented token-based systems without JWT or OAuth, I'd love to hear how you approached validation. Drop a comment or share your experience!
Subscribe to my newsletter
Read articles from Shanit Paul directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
