Backend Integration

Your backend server acts as a secure proxy between your frontend application and Redacto's API. This guide covers the required endpoints and security best practices.

Overview

Your backend needs to implement two key endpoints:

  1. Token Generation - Create JWT tokens for users
  2. Token Refresh - Renew expired tokens

These endpoints call Redacto's API using your CMS API Key and return tokens to your frontend.

Token Generation

Create an endpoint that generates JWT tokens for users.

Redacto API Endpoint

POST /consent/public/organisations/{organisation_uuid}/workspaces/{workspace_uuid}/tokens/generate

Request to Redacto

Headers:

X-CMS-API-KEY: {your-cms-api-key}
Content-Type: application/json

Body:

{
  "email": "[email protected]",
  "org_user_id": "user123",
  "mobile": "+1234567890",
  "expires_in_days": 1
}

At least one of email, org_user_id, or mobile must be provided.

Important: Token generation does not create consent users. Users are created when consent is submitted through the SDK, or when you explicitly call the User Management API. This allows you to generate tokens for anonymous IDs without creating user records until consent is actually given.

Response from Redacto

{
  "detail": {
    "token": "eyJhbGciOiJIUzI1NiIs...",
    "refresh_token": "eyJhbGciOiJIUzI1NiIs...",
    "expires_at": "2024-01-16T10:30:00Z"
  }
}

Token Refresh

Create an endpoint that refreshes expired access tokens.

Redacto API Endpoint

POST /consent/public/organisations/{organisation_uuid}/workspaces/{workspace_uuid}/tokens/refresh

Request to Redacto

Headers:

X-CMS-API-KEY: {your-cms-api-key}
Content-Type: application/json

Body:

{
  "refresh_token": "eyJhbGciOiJIUzI1NiIs..."
}

Response from Redacto

{
  "detail": {
    "token": "eyJhbGciOiJIUzI1NiIs...",
    "refresh_token": "eyJhbGciOiJIUzI1NiIs...",
    "expires_at": "2024-01-16T10:30:00Z"
  }
}

Note: Old tokens are invalidated when new tokens are generated.

Example Implementation

Express.js (Node.js)

import express, { Application, Request, Response } from "express";
import axios from "axios";

const app: Application = express();
app.use(express.json());

// Environment variables
const CMS_API_KEY = process.env.CMS_API_KEY;
const REDACTO_BASE_URL = process.env.REDACTO_BASE_URL || "https://api.redacto.io";

// Token generation endpoint
app.post(
  "/api/consent/token",
  async (req: Request, res: Response): Promise<void> => {
    try {
      const { email, org_user_id, mobile } = req.body;
      const { organisation_uuid, workspace_uuid } = req.query;

      if (!CMS_API_KEY) {
        res.status(500).json({ error: "CMS API Key not configured" });
        return;
      }

      if (!email && !org_user_id && !mobile) {
        res.status(400).json({ error: "At least one user identifier required" });
        return;
      }

      const response = await axios.post(
        `${REDACTO_BASE_URL}/consent/public/organisations/${organisation_uuid}/workspaces/${workspace_uuid}/tokens/generate`,
        {
          email,
          org_user_id,
          mobile,
          expires_in_days: 1,
        },
        {
          headers: {
            "X-CMS-API-Key": CMS_API_KEY,
            "Content-Type": "application/json",
          },
        }
      );

      res.json({
        token: response.data.detail.token,
        refresh_token: response.data.detail.refresh_token,
        expires_at: response.data.detail.expires_at,
      });
    } catch (error) {
      console.error("Error generating token:", error);
      res.status(500).json({ error: "Failed to generate token" });
    }
  }
);

// Token refresh endpoint
app.post(
  "/api/consent/refresh-token",
  async (req: Request, res: Response): Promise<void> => {
    try {
      const { refresh_token, organisation_uuid, workspace_uuid } = req.body;

      if (!CMS_API_KEY) {
        res.status(500).json({ error: "CMS API Key not configured" });
        return;
      }

      const response = await axios.post(
        `${REDACTO_BASE_URL}/consent/public/organisations/${organisation_uuid}/workspaces/${workspace_uuid}/tokens/refresh`,
        { refresh_token },
        {
          headers: {
            "X-CMS-API-Key": CMS_API_KEY,
            "Content-Type": "application/json",
          },
        }
      );

      res.json({
        token: response.data.detail.token,
        refresh_token: response.data.detail.refresh_token,
        expires_at: response.data.detail.expires_at,
      });
    } catch (error) {
      console.error("Error refreshing token:", error);
      res.status(500).json({ error: "Failed to refresh token" });
    }
  }
);

app.listen(3000, () => {
  console.log("Server running on port 3000");
});

Python (FastAPI)

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import httpx
import os

app = FastAPI()

CMS_API_KEY = os.getenv("CMS_API_KEY")
REDACTO_BASE_URL = os.getenv("REDACTO_BASE_URL", "https://api.redacto.io")


class TokenRequest(BaseModel):
    email: str | None = None
    org_user_id: str | None = None
    mobile: str | None = None
    organisation_uuid: str
    workspace_uuid: str


class RefreshRequest(BaseModel):
    refresh_token: str
    organisation_uuid: str
    workspace_uuid: str


@app.post("/api/consent/token")
async def generate_token(request: TokenRequest):
    if not CMS_API_KEY:
        raise HTTPException(status_code=500, detail="CMS API Key not configured")

    if not request.email and not request.org_user_id and not request.mobile:
        raise HTTPException(status_code=400, detail="At least one user identifier required")

    async with httpx.AsyncClient() as client:
        response = await client.post(
            f"{REDACTO_BASE_URL}/consent/public/organisations/{request.organisation_uuid}/workspaces/{request.workspace_uuid}/tokens/generate",
            json={
                "email": request.email,
                "org_user_id": request.org_user_id,
                "mobile": request.mobile,
                "expires_in_days": 1,
            },
            headers={
                "X-CMS-API-Key": CMS_API_KEY,
                "Content-Type": "application/json",
            },
        )

    if response.status_code != 200:
        raise HTTPException(status_code=500, detail="Failed to generate token")

    data = response.json()["detail"]
    return {
        "token": data["token"],
        "refresh_token": data["refresh_token"],
        "expires_at": data["expires_at"],
    }


@app.post("/api/consent/refresh-token")
async def refresh_token(request: RefreshRequest):
    if not CMS_API_KEY:
        raise HTTPException(status_code=500, detail="CMS API Key not configured")

    async with httpx.AsyncClient() as client:
        response = await client.post(
            f"{REDACTO_BASE_URL}/consent/public/organisations/{request.organisation_uuid}/workspaces/{request.workspace_uuid}/tokens/refresh",
            json={"refresh_token": request.refresh_token},
            headers={
                "X-CMS-API-Key": CMS_API_KEY,
                "Content-Type": "application/json",
            },
        )

    if response.status_code != 200:
        raise HTTPException(status_code=500, detail="Failed to refresh token")

    data = response.json()["detail"]
    return {
        "token": data["token"],
        "refresh_token": data["refresh_token"],
        "expires_at": data["expires_at"],
    }

Django

import requests
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.conf import settings
import json


@csrf_exempt
def generate_token(request):
    if request.method != "POST":
        return JsonResponse({"error": "Method not allowed"}, status=405)

    cms_api_key = settings.CMS_API_KEY
    base_url = getattr(settings, "REDACTO_BASE_URL", "https://api.redacto.io")

    if not cms_api_key:
        return JsonResponse({"error": "CMS API Key not configured"}, status=500)

    data = json.loads(request.body)
    org_uuid = request.GET.get("organisation_uuid")
    ws_uuid = request.GET.get("workspace_uuid")

    if not data.get("email") and not data.get("org_user_id") and not data.get("mobile"):
        return JsonResponse({"error": "At least one user identifier required"}, status=400)

    response = requests.post(
        f"{base_url}/consent/public/organisations/{org_uuid}/workspaces/{ws_uuid}/tokens/generate",
        json={
            "email": data.get("email"),
            "org_user_id": data.get("org_user_id"),
            "mobile": data.get("mobile"),
            "expires_in_days": 1,
        },
        headers={
            "X-CMS-API-Key": cms_api_key,
            "Content-Type": "application/json",
        },
    )

    if response.status_code != 200:
        return JsonResponse({"error": "Failed to generate token"}, status=500)

    detail = response.json()["detail"]
    return JsonResponse({
        "token": detail["token"],
        "refresh_token": detail["refresh_token"],
        "expires_at": detail["expires_at"],
    })


@csrf_exempt
def refresh_token(request):
    if request.method != "POST":
        return JsonResponse({"error": "Method not allowed"}, status=405)

    cms_api_key = settings.CMS_API_KEY
    base_url = getattr(settings, "REDACTO_BASE_URL", "https://api.redacto.io")

    if not cms_api_key:
        return JsonResponse({"error": "CMS API Key not configured"}, status=500)

    data = json.loads(request.body)

    response = requests.post(
        f"{base_url}/consent/public/organisations/{data['organisation_uuid']}/workspaces/{data['workspace_uuid']}/tokens/refresh",
        json={"refresh_token": data["refresh_token"]},
        headers={
            "X-CMS-API-Key": cms_api_key,
            "Content-Type": "application/json",
        },
    )

    if response.status_code != 200:
        return JsonResponse({"error": "Failed to refresh token"}, status=500)

    detail = response.json()["detail"]
    return JsonResponse({
        "token": detail["token"],
        "refresh_token": detail["refresh_token"],
        "expires_at": detail["expires_at"],
    })

Security Best Practices

1. Protect Your API Key

  • Never expose the CMS API Key to the frontend
  • Store it in environment variables, not in code
  • Use secrets management in production (e.g., AWS Secrets Manager, HashiCorp Vault)
# .env file (never commit to version control)
CMS_API_KEY=your-secret-key-here

2. Validate User Identity (For User Management APIs)

User identity validation is not required for token generation - you can generate tokens for anonymous IDs, emails, or any identifier. However, you should validate user identity before calling User Management APIs (create, update, link users) to prevent unauthorized modifications:

// Token generation - no validation required (can use anonymous IDs)
app.post("/api/consent/token", async (req, res) => {
  const { identifier } = req.body; // Could be anonymous ID, email, etc.

  const response = await generateRedactoToken({
    org_user_id: identifier,
  });

  res.json(response);
});

// User Management APIs - validate identity first
app.post("/api/consent/create-user", async (req, res) => {
  // Verify user is authenticated before modifying user records
  const authenticatedUser = await authenticateRequest(req);
  if (!authenticatedUser) {
    return res.status(401).json({ error: "Unauthorized" });
  }

  // Now safe to create/update/link consent users
  const response = await createConsentUser({
    org_user_id: authenticatedUser.id,
    primary_email: authenticatedUser.email,
  });

  res.json(response);
});

3. Secure Token Transmission

  • Use HTTPS for all API calls
  • Consider using httpOnly cookies for token storage
  • Implement proper CORS configuration
import cors from "cors";

app.use(cors({
  origin: ["https://your-frontend.com"],
  credentials: true,
}));

4. Handle Token Expiration

Implement automatic token refresh in your frontend:

async function fetchWithAuth(url: string, options: RequestInit = {}) {
  let response = await fetch(url, {
    ...options,
    headers: {
      ...options.headers,
      Authorization: `Bearer ${getAccessToken()}`,
    },
  });

  if (response.status === 401) {
    // Token expired, try to refresh
    await refreshTokens();
    response = await fetch(url, {
      ...options,
      headers: {
        ...options.headers,
        Authorization: `Bearer ${getAccessToken()}`,
      },
    });
  }

  return response;
}

5. Rate Limiting

Protect your token endpoints from abuse:

import rateLimit from "express-rate-limit";

const tokenLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per window
});

app.use("/api/consent/token", tokenLimiter);