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

Shanit PaulShanit Paul
3 min read

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.

💡
This post walks through that token generation and validation process.

Why Use Encrypted Tokens (Instead of Raw UUIDs)?

  1. Raw UUIDs are guessable if exposed in patterns.

  2. They can be tampered with if not verified properly.

  3. 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!

0
Subscribe to my newsletter

Read articles from Shanit Paul directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Shanit Paul
Shanit Paul