Signature Verification

View as Markdown

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.

1{
2 "alg": "RS256", // (1)!
3 "typ": "JWT" // (2)!
4}
  1. Always RS256 (RSA with SHA-256)
  2. Token type, always JWT
1{
2 "iss": "brij.fi", // (1)!
3 "aud": "your-partner-id", // (2)!
4 "iat": 1700000000, // (3)!
5 "exp": 1700000600, // (4)!
6 "jti": "f47ac10b-58cc-4372-a567-0e02b2c3d479", // (5)!
7 "payload_hash": "a1b2c3d4e5f6789..." // (6)!
8}
  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:

Production

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0JTgjx1M34EzEn8nZ+MH
rjlE6wh0698MHeTsC9GUQX4SdhDLi02R+r6i7hknhfgCmTVDHyfvoCAp0Jm8G0nI
OkgsA2VrBxLAZoCtQyRBz26+0nkQcWNtg4s4MtyQmCrrz/hrl6CU0IhiKKfSPf/i
DeIXFJd7xyQQdHaBfl8ZQqdvZJeJJLlqYOHPn8vtq6ubpeFfA43jbeSyS+VzBfMM
eAHu1HvG54oYJEYBNsHKe7ZMJvgX36I31aOFlMEuX4wjittBqdlGa4CvB7P2sc0V
t2cp6QVbxjSAIA1bc6uM5dhCrC7fuZ4HNGqWWNK7yfqUsWrDsCRr3QjQiogbIQu4
oQIDAQAB
-----END PUBLIC KEY-----

Demo

-----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

Node.js / TypeScript

1import jwt from 'jsonwebtoken';
2import crypto from 'crypto';
3
4// BRIJ public key (use appropriate key for your environment)
5const BRIJ_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
6[YOUR_ENVIRONMENT_PUBLIC_KEY]
7-----END PUBLIC KEY-----`;
8
9const YOUR_PARTNER_ID = 'your-partner-id';
10
11interface BrijJwtPayload {
12 iss: string;
13 aud: string;
14 iat: number;
15 exp: number;
16 jti: string;
17 payload_hash: string;
18}
19
20function verifyBrijRequest(
21 signatureHeader: string,
22 requestBody: Buffer | string
23): { valid: boolean; jti?: string; error?: string } {
24 try {
25 // 1. Verify JWT signature and decode
26 const decoded = jwt.verify(signatureHeader, BRIJ_PUBLIC_KEY, {
27 algorithms: ['RS256'],
28 issuer: 'brij.fi',
29 audience: YOUR_PARTNER_ID,
30 }) as BrijJwtPayload;
31
32 // 2. Verify payload hash
33 const bodyBuffer = typeof requestBody === 'string'
34 ? Buffer.from(requestBody)
35 : requestBody;
36 const calculatedHash = crypto
37 .createHash('sha256')
38 .update(bodyBuffer)
39 .digest('hex');
40
41 if (calculatedHash !== decoded.payload_hash) {
42 return {valid: false, error: 'Payload hash mismatch'};
43 }
44
45 // 3. Return success with jti for idempotency tracking
46 return {valid: true, jti: decoded.jti};
47 } catch (error) {
48 return {
49 valid: false,
50 error: error instanceof Error ? error.message : 'Unknown error'
51 };
52 }
53}
54
55// Express.js middleware example
56import {Request, Response, NextFunction} from 'express';
57
58function brijSignatureMiddleware(
59 req: Request,
60 res: Response,
61 next: NextFunction
62) {
63 const signature = req.headers['x-brij-signature'] as string;
64
65 if (!signature) {
66 return res.status(401).json({error: 'Missing X-BRIJ-Signature header'});
67 }
68
69 // Note: You need raw body access - configure express accordingly
70 const result = verifyBrijRequest(signature, req.body);
71
72 if (!result.valid) {
73 return res.status(401).json({error: result.error});
74 }
75
76 // Optionally check jti for replay protection
77 // if (seenJtis.has(result.jti)) {
78 // return res.status(409).json({ error: 'Duplicate request' });
79 // }
80 // seenJtis.add(result.jti);
81
82 next();
83}

Python

1import hashlib
2import jwt
3from typing import Tuple, Optional
4
5# BRIJ public key (use appropriate key for your environment)
6BRIJ_PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
7[YOUR_ENVIRONMENT_PUBLIC_KEY]
8-----END PUBLIC KEY-----"""
9
10YOUR_PARTNER_ID = "your-partner-id"
11
12
13def verify_brij_request(
14 signature_header: str,
15 request_body: bytes
16) -> Tuple[bool, Optional[str], Optional[str]]:
17 """
18 Verify a BRIJ request signature.
19
20 Returns:
21 Tuple of (is_valid, jti, error_message)
22 """
23 try:
24 # 1. Verify JWT signature and decode
25 decoded = jwt.decode(
26 signature_header,
27 BRIJ_PUBLIC_KEY,
28 algorithms=["RS256"],
29 issuer="brij.fi",
30 audience=YOUR_PARTNER_ID,
31 )
32
33 # 2. Verify payload hash
34 calculated_hash = hashlib.sha256(request_body).hexdigest()
35
36 if calculated_hash != decoded["payload_hash"]:
37 return False, None, "Payload hash mismatch"
38
39 # 3. Return success with jti for idempotency tracking
40 return True, decoded["jti"], None
41
42 except jwt.ExpiredSignatureError:
43 return False, None, "Token expired"
44 except jwt.InvalidAudienceError:
45 return False, None, "Invalid audience"
46 except jwt.InvalidIssuerError:
47 return False, None, "Invalid issuer"
48 except jwt.InvalidSignatureError:
49 return False, None, "Invalid signature"
50 except Exception as e:
51 return False, None, str(e)
52
53
54# ------------------------------
55# Flask example
56# ------------------------------
57from flask import Flask, request, jsonify
58
59flask_app = Flask(__name__)
60
61
62@flask_app.before_request
63def verify_brij_signature():
64 # Skip verification for non-BRIJ endpoints
65 if not request.path.startswith("/webhook"):
66 return
67
68 signature = request.headers.get("X-BRIJ-Signature")
69
70 if not signature:
71 return jsonify({"error": "Missing X-BRIJ-Signature header"}), 401
72
73 is_valid, jti, error = verify_brij_request(signature, request.get_data())
74
75 if not is_valid:
76 return jsonify({"error": error}), 401
77
78 # Optionally store jti in request context for replay protection
79 request.brij_jti = jti
80
81
82# ------------------------------
83# FastAPI example (alternative)
84# ------------------------------
85from fastapi import FastAPI, Request, HTTPException, Depends
86
87fastapi_app = FastAPI()
88
89
90async def verify_brij_signature(request: Request) -> str:
91 signature = request.headers.get("X-BRIJ-Signature")
92
93 if not signature:
94 raise HTTPException(status_code=401, detail="Missing X-BRIJ-Signature header")
95
96 body = await request.body()
97 is_valid, jti, error = verify_brij_request(signature, body)
98
99 if not is_valid:
100 raise HTTPException(status_code=401, detail=error)
101
102 return jti
103
104
105@fastapi_app.post("/webhook")
106async def webhook(request: Request, jti: str = Depends(verify_brij_signature)):
107 # Process webhook...
108 return {"status": "ok", "request_id": jti}

Go

1package main
2
3import (
4 "bytes"
5 "context"
6 "crypto/sha256"
7 "encoding/hex"
8 "fmt"
9 "io"
10 "net/http"
11
12 "github.com/golang-jwt/jwt/v5"
13)
14
15// BRIJ public key (use appropriate key for your environment)
16var brijPublicKeyPEM = `-----BEGIN PUBLIC KEY-----
17[YOUR_ENVIRONMENT_PUBLIC_KEY]
18-----END PUBLIC KEY-----`
19
20const yourPartnerID = "your-partner-id"
21
22type BrijClaims struct {
23 PayloadHash string `json:"payload_hash"`
24 jwt.RegisteredClaims
25}
26
27func VerifyBrijRequest(signatureHeader string, requestBody []byte) (string, error) {
28 // Parse the public key
29 publicKey, err := jwt.ParseRSAPublicKeyFromPEM([]byte(brijPublicKeyPEM))
30 if err != nil {
31 return "", fmt.Errorf("failed to parse public key: %w", err)
32 }
33
34 // Parse and verify the JWT
35 token, err := jwt.ParseWithClaims(
36 signatureHeader, &BrijClaims{}, func(token *jwt.Token) (interface{}, error) {
37 // Verify the signing method
38 if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
39 return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
40 }
41 return publicKey, nil
42 },
43 )
44
45 if err != nil {
46 return "", fmt.Errorf("failed to parse token: %w", err)
47 }
48
49 claims, ok := token.Claims.(*BrijClaims)
50 if !ok || !token.Valid {
51 return "", fmt.Errorf("invalid token")
52 }
53
54 // Verify issuer
55 if claims.Issuer != "brij.fi" {
56 return "", fmt.Errorf("invalid issuer: %s", claims.Issuer)
57 }
58
59 // Verify audience
60 if !claims.VerifyAudience(yourPartnerID, true) {
61 return "", fmt.Errorf("invalid audience")
62 }
63
64 // Verify payload hash
65 hash := sha256.Sum256(requestBody)
66 calculatedHash := hex.EncodeToString(hash[:])
67
68 if calculatedHash != claims.PayloadHash {
69 return "", fmt.Errorf("payload hash mismatch")
70 }
71
72 return claims.ID, nil // Return jti for idempotency tracking
73}
74
75// HTTP middleware example
76func BrijSignatureMiddleware(next http.Handler) http.Handler {
77 return http.HandlerFunc(
78 func(w http.ResponseWriter, r *http.Request) {
79 signature := r.Header.Get("X-BRIJ-Signature")
80 if signature == "" {
81 http.Error(w, "Missing X-BRIJ-Signature header", http.StatusUnauthorized)
82 return
83 }
84
85 // Read the body
86 body, err := io.ReadAll(r.Body)
87 if err != nil {
88 http.Error(w, "Failed to read body", http.StatusBadRequest)
89 return
90 }
91
92 // Verify the signature
93 jti, err := VerifyBrijRequest(signature, body)
94 if err != nil {
95 http.Error(w, err.Error(), http.StatusUnauthorized)
96 return
97 }
98
99 // Optionally check jti for replay protection
100 // if seenJtis[jti] {
101 // http.Error(w, "Duplicate request", http.StatusConflict)
102 // return
103 // }
104 // seenJtis[jti] = true
105
106 // Store body back for handler to read
107 r.Body = io.NopCloser(bytes.NewBuffer(body))
108
109 // Add jti to context if needed
110 ctx := context.WithValue(r.Context(), "brij_jti", jti)
111 next.ServeHTTP(w, r.WithContext(ctx))
112 },
113 )
114}

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