# 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.
```json5
{
"alg": "RS256", // Always `RS256` (RSA with SHA-256)
"typ": "JWT" // Token type, always `JWT`
}
```
```json5
{
"iss": "brij.fi", // Issuer - always `brij.fi`
"aud": "your-partner-id", // Audience - your partner ID
"iat": 1700000000, // Issued at - Unix timestamp (seconds) when the token was created
"exp": 1700000600, // Expiration - Unix timestamp (seconds), 10 minutes after `iat`
"jti": "f47ac10b-58cc-4372-a567-0e02b2c3d479", // JWT ID - unique UUID for this request (use for idempotency)
"payload_hash": "a1b2c3d4e5f6789..." // 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
```typescript
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();
}
```
```python
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}
```
```go
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