Skip to content

Signature Verification

This document explains how to verify that API requests and webhooks from BRIJ are authentic and have not been tampered with.

Overview

All requests from BRIJ to your endpoints (API calls and webhooks) include a cryptographic signature in the X-BRIJ-Signature HTTP header. This signature is a JSON Web Token (JWT) signed with BRIJ's private key using the RS256 algorithm.

By verifying this signature, you can ensure that:

  • The request originated from BRIJ
  • The request payload has not been modified in transit
  • The request is not a replay of a previous request

Signature Format

HTTP Header

X-BRIJ-Signature: eyJhbGciOiJSUzI1NiIsImtpZCI6ImtleS0xIiwidHlwIjoiSldUIn0...

JWT Structure

The signature is a standard JWT with three parts: header, payload, and signature.

Header
{
  "alg": "RS256", // (1)!
  "typ": "JWT" // (2)!
}
  1. Always RS256 (RSA with SHA-256)
  2. Token type, always JWT
Payload (Claims)
{
  "iss": "brij.fi", // (1)!
  "aud": "your-partner-id", // (2)!
  "iat": 1700000000, // (3)!
  "exp": 1700000600, // (4)!
  "jti": "f47ac10b-58cc-4372-a567-0e02b2c3d479", // (5)!
  "payload_hash": "a1b2c3d4e5f6789..." // (6)!
}
  1. Issuer - always brij.fi
  2. Audience - your partner ID
  3. Issued at - Unix timestamp (seconds) when the token was created
  4. Expiration - Unix timestamp (seconds), 10 minutes after iat
  5. JWT ID - unique UUID for this request (use for idempotency)
  6. SHA-256 hash of the request body, hex-encoded

Verification Steps

Follow these steps to verify an incoming request:

1. Extract the JWT

Get the X-BRIJ-Signature header from the incoming request.

2. Verify the JWT Signature

Using BRIJ's public key (see below), verify the JWT signature. This confirms the token was signed by BRIJ.

3. Validate Claims

After signature verification, validate the following claims:

  • iss must equal brij.fi
  • aud must equal your partner ID
  • exp must be greater than the current Unix timestamp (token not expired)

4. Verify Payload Integrity

  1. Read the raw request body
  2. Calculate the SHA-256 hash of the body
  3. Hex-encode the hash
  4. Compare with the payload_hash claim

If they match, the payload has not been tampered with.

5. (Optional) Check for Replay Attacks

Store the jti claim and reject any requests with a previously seen jti. This prevents replay attacks.

Public Keys

Use the appropriate public key based on your environment:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0JTgjx1M34EzEn8nZ+MH
rjlE6wh0698MHeTsC9GUQX4SdhDLi02R+r6i7hknhfgCmTVDHyfvoCAp0Jm8G0nI
OkgsA2VrBxLAZoCtQyRBz26+0nkQcWNtg4s4MtyQmCrrz/hrl6CU0IhiKKfSPf/i
DeIXFJd7xyQQdHaBfl8ZQqdvZJeJJLlqYOHPn8vtq6ubpeFfA43jbeSyS+VzBfMM
eAHu1HvG54oYJEYBNsHKe7ZMJvgX36I31aOFlMEuX4wjittBqdlGa4CvB7P2sc0V
t2cp6QVbxjSAIA1bc6uM5dhCrC7fuZ4HNGqWWNK7yfqUsWrDsCRr3QjQiogbIQu4
oQIDAQAB
-----END PUBLIC KEY-----
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArPPIIDwHo5DEM7b+uor+
bYEhwjo1C3QGf7/QkQadsop1Qee7+bW3df2o4YANyWVZIj+0oWT8RsjQGmdNzvDp
qQzRYYHy08ZmU3v/1pPXE4JpmMuXxKZ6tJDagn7poeaWpGdm3BKLyd8qNf4ZXs8q
G3BoqBKjwaDpK2aYPAgRSoPT6iT7K2i7dfhJV5pjW4lUXqwGcVNBn7pxbkZGwj1T
EHDIEUWgdq9L3Swo1hI2/fIa0KFa7wiKPWALyounRUGCEcOwpHG32k+L8odoAuLe
NSjyNIDYXuNWzICuyGq/Glotf+FTrGlMT634v7nRC/qX5BGhZK3SA6KBz5O+hncH
rwIDAQAB
-----END PUBLIC KEY-----

Code Examples

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import jwt from 'jsonwebtoken';
import crypto from 'crypto';

// BRIJ public key (use appropriate key for your environment)
const BRIJ_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
[YOUR_ENVIRONMENT_PUBLIC_KEY]
-----END PUBLIC KEY-----`;

const YOUR_PARTNER_ID = 'your-partner-id';

interface BrijJwtPayload {
    iss: string;
    aud: string;
    iat: number;
    exp: number;
    jti: string;
    payload_hash: string;
}

function verifyBrijRequest(
    signatureHeader: string,
    requestBody: Buffer | string
): { valid: boolean; jti?: string; error?: string } {
    try {
        // 1. Verify JWT signature and decode
        const decoded = jwt.verify(signatureHeader, BRIJ_PUBLIC_KEY, {
            algorithms: ['RS256'],
            issuer: 'brij.fi',
            audience: YOUR_PARTNER_ID,
        }) as BrijJwtPayload;

        // 2. Verify payload hash
        const bodyBuffer = typeof requestBody === 'string'
            ? Buffer.from(requestBody)
            : requestBody;
        const calculatedHash = crypto
            .createHash('sha256')
            .update(bodyBuffer)
            .digest('hex');

        if (calculatedHash !== decoded.payload_hash) {
            return {valid: false, error: 'Payload hash mismatch'};
        }

        // 3. Return success with jti for idempotency tracking
        return {valid: true, jti: decoded.jti};
    } catch (error) {
        return {
            valid: false,
            error: error instanceof Error ? error.message : 'Unknown error'
        };
    }
}

// Express.js middleware example
import {Request, Response, NextFunction} from 'express';

function brijSignatureMiddleware(
    req: Request,
    res: Response,
    next: NextFunction
) {
    const signature = req.headers['x-brij-signature'] as string;

    if (!signature) {
        return res.status(401).json({error: 'Missing X-BRIJ-Signature header'});
    }

    // Note: You need raw body access - configure express accordingly
    const result = verifyBrijRequest(signature, req.body);

    if (!result.valid) {
        return res.status(401).json({error: result.error});
    }

    // Optionally check jti for replay protection
    // if (seenJtis.has(result.jti)) {
    //   return res.status(409).json({ error: 'Duplicate request' });
    // }
    // seenJtis.add(result.jti);

    next();
}
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
import hashlib
import jwt
from typing import Tuple, Optional

# BRIJ public key (use appropriate key for your environment)
BRIJ_PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
[YOUR_ENVIRONMENT_PUBLIC_KEY]
-----END PUBLIC KEY-----"""

YOUR_PARTNER_ID = "your-partner-id"


def verify_brij_request(
        signature_header: str,
        request_body: bytes
) -> Tuple[bool, Optional[str], Optional[str]]:
    """
    Verify a BRIJ request signature.

    Returns:
        Tuple of (is_valid, jti, error_message)
    """
    try:
        # 1. Verify JWT signature and decode
        decoded = jwt.decode(
            signature_header,
            BRIJ_PUBLIC_KEY,
            algorithms=["RS256"],
            issuer="brij.fi",
            audience=YOUR_PARTNER_ID,
        )

        # 2. Verify payload hash
        calculated_hash = hashlib.sha256(request_body).hexdigest()

        if calculated_hash != decoded["payload_hash"]:
            return False, None, "Payload hash mismatch"

        # 3. Return success with jti for idempotency tracking
        return True, decoded["jti"], None

    except jwt.ExpiredSignatureError:
        return False, None, "Token expired"
    except jwt.InvalidAudienceError:
        return False, None, "Invalid audience"
    except jwt.InvalidIssuerError:
        return False, None, "Invalid issuer"
    except jwt.InvalidSignatureError:
        return False, None, "Invalid signature"
    except Exception as e:
        return False, None, str(e)


# ------------------------------
# Flask example
# ------------------------------
from flask import Flask, request, jsonify

flask_app = Flask(__name__)


@flask_app.before_request
def verify_brij_signature():
    # Skip verification for non-BRIJ endpoints
    if not request.path.startswith("/webhook"):
        return

    signature = request.headers.get("X-BRIJ-Signature")

    if not signature:
        return jsonify({"error": "Missing X-BRIJ-Signature header"}), 401

    is_valid, jti, error = verify_brij_request(signature, request.get_data())

    if not is_valid:
        return jsonify({"error": error}), 401

    # Optionally store jti in request context for replay protection
    request.brij_jti = jti


# ------------------------------
# FastAPI example (alternative)
# ------------------------------
from fastapi import FastAPI, Request, HTTPException, Depends

fastapi_app = FastAPI()


async def verify_brij_signature(request: Request) -> str:
    signature = request.headers.get("X-BRIJ-Signature")

    if not signature:
        raise HTTPException(status_code=401, detail="Missing X-BRIJ-Signature header")

    body = await request.body()
    is_valid, jti, error = verify_brij_request(signature, body)

    if not is_valid:
        raise HTTPException(status_code=401, detail=error)

    return jti


@fastapi_app.post("/webhook")
async def webhook(request: Request, jti: str = Depends(verify_brij_signature)):
    # Process webhook...
    return {"status": "ok", "request_id": jti}
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
package main

import (
    "bytes"
    "context"
    "crypto/sha256"
    "encoding/hex"
    "fmt"
    "io"
    "net/http"

    "github.com/golang-jwt/jwt/v5"
)

// BRIJ public key (use appropriate key for your environment)
var brijPublicKeyPEM = `-----BEGIN PUBLIC KEY-----
[YOUR_ENVIRONMENT_PUBLIC_KEY]
-----END PUBLIC KEY-----`

const yourPartnerID = "your-partner-id"

type BrijClaims struct {
    PayloadHash string `json:"payload_hash"`
    jwt.RegisteredClaims
}

func VerifyBrijRequest(signatureHeader string, requestBody []byte) (string, error) {
    // Parse the public key
    publicKey, err := jwt.ParseRSAPublicKeyFromPEM([]byte(brijPublicKeyPEM))
    if err != nil {
        return "", fmt.Errorf("failed to parse public key: %w", err)
    }

    // Parse and verify the JWT
    token, err := jwt.ParseWithClaims(
        signatureHeader, &BrijClaims{}, func(token *jwt.Token) (interface{}, error) {
            // Verify the signing method
            if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
                return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
            }
            return publicKey, nil
        },
    )

    if err != nil {
        return "", fmt.Errorf("failed to parse token: %w", err)
    }

    claims, ok := token.Claims.(*BrijClaims)
    if !ok || !token.Valid {
        return "", fmt.Errorf("invalid token")
    }

    // Verify issuer
    if claims.Issuer != "brij.fi" {
        return "", fmt.Errorf("invalid issuer: %s", claims.Issuer)
    }

    // Verify audience
    if !claims.VerifyAudience(yourPartnerID, true) {
        return "", fmt.Errorf("invalid audience")
    }

    // Verify payload hash
    hash := sha256.Sum256(requestBody)
    calculatedHash := hex.EncodeToString(hash[:])

    if calculatedHash != claims.PayloadHash {
        return "", fmt.Errorf("payload hash mismatch")
    }

    return claims.ID, nil // Return jti for idempotency tracking
}

// HTTP middleware example
func BrijSignatureMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(
        func(w http.ResponseWriter, r *http.Request) {
            signature := r.Header.Get("X-BRIJ-Signature")
            if signature == "" {
                http.Error(w, "Missing X-BRIJ-Signature header", http.StatusUnauthorized)
                return
            }

            // Read the body
            body, err := io.ReadAll(r.Body)
            if err != nil {
                http.Error(w, "Failed to read body", http.StatusBadRequest)
                return
            }

            // Verify the signature
            jti, err := VerifyBrijRequest(signature, body)
            if err != nil {
                http.Error(w, err.Error(), http.StatusUnauthorized)
                return
            }

            // Optionally check jti for replay protection
            // if seenJtis[jti] {
            //     http.Error(w, "Duplicate request", http.StatusConflict)
            //     return
            // }
            // seenJtis[jti] = true

            // Store body back for handler to read
            r.Body = io.NopCloser(bytes.NewBuffer(body))

            // Add jti to context if needed
            ctx := context.WithValue(r.Context(), "brij_jti", jti)
            next.ServeHTTP(w, r.WithContext(ctx))
        },
    )
}

Security Best Practices

Always Verify Signatures

Never process a request from BRIJ without first verifying the signature. Unverified requests could be forged by malicious actors.

Reject Expired Tokens

The exp claim provides protection against replay attacks with old tokens. Always check that the token has not expired.

Implement Idempotency

Use the jti (JWT ID) claim to detect and reject duplicate requests:

  • Store seen jti values (e.g., in Redis with TTL matching token expiration)
  • Reject requests with previously seen jti values
  • This prevents replay attacks even with valid, non-expired tokens

Use the Correct Public Key

Make sure to use the appropriate public key for your environment:

  • Use the Demo public key for testing and development
  • Use the Production public key for live transactions

Key Management

  • Perform signature verification server-side, never in client-side code
  • Subscribe to BRIJ notifications for key rotation announcements

Troubleshooting

"Invalid signature" Error

  • Ensure you're using the correct public key for the environment
  • Verify the JWT string is complete and not truncated

"Token expired" Error

  • Check if your server's clock is synchronized (use NTP)
  • Tokens are valid for 10 minutes from issuance
  • If requests consistently expire, investigate network latency

"Payload hash mismatch" Error

  • Ensure you're hashing the raw request body before any parsing
  • The hash must be calculated on the exact bytes received
  • Check for any middleware that modifies the body before verification

"Invalid audience" Error

  • Verify your partner ID matches what BRIJ has configured
  • Partner IDs are case-sensitive

Missing Header

If you don't see the X-BRIJ-Signature header:

  • Check if your load balancer or proxy is stripping custom headers
  • Verify the header name is case-insensitive in your framework

Support

If you encounter issues with signature verification, contact BRIJ support with:

  • The full JWT token (from X-BRIJ-Signature header)
  • The raw request body
  • Your partner ID
  • The error message you're receiving