Custom Claims

Add custom data to JWT tokens to implement business-specific logic and enrich device attestation with your application's context.

Availability

Custom claims are available for Professional tier and above. The number of custom claims and complexity depends on your service tier.

What are Custom Claims?

Custom claims allow you to embed additional data in the JWT tokens issued by Grantiva. This data can include:

  • User-specific permissions and roles
  • Feature flags and access levels
  • Business logic parameters
  • Subscription tier information
  • Geographic restrictions
  • Custom risk adjustments

Configuring Custom Claims

1. Define Claim Rules

Custom claims are configured through the Grantiva dashboard or API. You can define rules based on:

{
  "customClaimsRules": [
    {
      "name": "user_tier",
      "type": "static",
      "value": "premium"
    },
    {
      "name": "access_level",
      "type": "conditional",
      "conditions": [
        {
          "if": { "riskScore": { "lessThan": 30 } },
          "then": "full"
        },
        {
          "if": { "riskScore": { "between": [30, 70] } },
          "then": "limited"
        },
        {
          "else": "readonly"
        }
      ]
    },
    {
      "name": "features",
      "type": "dynamic",
      "source": "device_profile",
      "mapping": {
        "enablePayments": "{{ riskScore < 50 }}",
        "enableTransfers": "{{ riskScore < 30 && attestationCount > 5 }}",
        "maxTransactionAmount": "{{ riskScore < 20 ? 10000 : 1000 }}"
      }
    }
  ]
}

2. Claim Types

Type Description Use Case
Static Fixed values that don't change App version, environment
Conditional Values based on device attributes Access levels, permissions
Dynamic Computed values using expressions Complex business logic
External Values from external API calls User profiles, subscriptions

Implementation Examples

Risk-Based Access Control

{
  "customClaimsRules": [
    {
      "name": "permissions",
      "type": "conditional",
      "conditions": [
        {
          "if": {
            "and": [
              { "riskScore": { "lessThan": 20 } },
              { "jailbreakDetected": false },
              { "attestationCount": { "greaterThan": 10 } }
            ]
          },
          "then": ["read", "write", "delete", "admin"]
        },
        {
          "if": { "riskScore": { "lessThan": 50 } },
          "then": ["read", "write"]
        },
        {
          "else": ["read"]
        }
      ]
    }
  ]
}

Subscription Tier Management

{
  "customClaimsRules": [
    {
      "name": "subscription",
      "type": "external",
      "endpoint": "https://api.yourapp.com/user/subscription",
      "headers": {
        "X-Device-ID": "{{ deviceId }}"
      },
      "cache": {
        "ttl": 3600,
        "key": "sub_{{ deviceId }}"
      }
    },
    {
      "name": "features",
      "type": "dynamic",
      "source": "subscription",
      "mapping": {
        "premiumFeatures": "{{ subscription.tier == 'premium' }}",
        "apiRateLimit": "{{ subscription.tier == 'premium' ? 10000 : 1000 }}",
        "storageQuota": "{{ subscription.tier == 'premium' ? '100GB' : '10GB' }}"
      }
    }
  ]
}

Geographic Restrictions

{
  "customClaimsRules": [
    {
      "name": "geo_compliance",
      "type": "dynamic",
      "mapping": {
        "allowedRegions": ["US", "CA", "EU"],
        "currentRegion": "{{ geoip.country }}",
        "isAllowed": "{{ geoip.country in allowedRegions }}",
        "requiresConsent": "{{ geoip.country in ['EU', 'UK'] }}"
      }
    },
    {
      "name": "data_residency",
      "type": "conditional",
      "conditions": [
        {
          "if": { "geoip.continent": "EU" },
          "then": "eu-west-1"
        },
        {
          "if": { "geoip.country": "CA" },
          "then": "ca-central-1"
        },
        {
          "else": "us-east-1"
        }
      ]
    }
  ]
}

Using Custom Claims in Your App

iOS Client

// Decode custom claims from JWT
struct CustomClaims: Codable {
    let permissions: [String]
    let subscription: SubscriptionInfo
    let features: Features
    let geoCompliance: GeoCompliance
}

func decodeToken(_ token: String) throws -> CustomClaims {
    let segments = token.components(separatedBy: ".")
    guard segments.count == 3 else {
        throw TokenError.invalidFormat
    }
    
    let payloadData = Data(base64Encoded: segments[1])!
    let payload = try JSONDecoder().decode(JWTPayload.self, from: payloadData)
    
    return payload.customClaims
}

// Use claims for access control
func canPerformAction(_ action: String) -> Bool {
    guard let claims = currentClaims else { return false }
    
    switch action {
    case "transfer":
        return claims.permissions.contains("transfer") && 
               claims.features.enableTransfers
    case "premium_feature":
        return claims.subscription.tier == "premium"
    default:
        return claims.permissions.contains(action)
    }
}

Server Validation

// Node.js example
const jwt = require('jsonwebtoken');

function validateRequest(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    
    // Check custom claims
    const { customClaims } = decoded;
    
    // Enforce geographic restrictions
    if (customClaims.geoCompliance && !customClaims.geoCompliance.isAllowed) {
      return res.status(403).json({ 
        error: 'Service not available in your region' 
      });
    }
    
    // Check permissions
    req.user = {
      ...decoded,
      permissions: customClaims.permissions,
      features: customClaims.features
    };
    
    next();
  } catch (error) {
    res.status(401).json({ error: 'Invalid token' });
  }
}

// Route with custom claim validation
app.post('/api/transfer', validateRequest, (req, res) => {
  if (!req.user.permissions.includes('transfer')) {
    return res.status(403).json({ error: 'Transfer permission required' });
  }
  
  const maxAmount = req.user.features.maxTransactionAmount || 1000;
  if (req.body.amount > maxAmount) {
    return res.status(400).json({ 
      error: `Amount exceeds limit of ${maxAmount}` 
    });
  }
  
  // Process transfer...
});

Advanced Patterns

Time-Based Claims

{
  "customClaimsRules": [
    {
      "name": "temporal_access",
      "type": "dynamic",
      "mapping": {
        "isBusinessHours": "{{ now.hour >= 9 && now.hour < 17 }}",
        "isWeekday": "{{ now.dayOfWeek >= 1 && now.dayOfWeek <= 5 }}",
        "maintenanceMode": "{{ now.hour >= 2 && now.hour < 4 }}",
        "allowHighValueTransactions": "{{ isBusinessHours && isWeekday }}"
      }
    }
  ]
}

Device History Claims

{
  "customClaimsRules": [
    {
      "name": "trust_score",
      "type": "dynamic",
      "mapping": {
        "deviceAge": "{{ daysSince(firstSeen) }}",
        "attestationFrequency": "{{ attestationCount / deviceAge }}",
        "riskTrend": "{{ riskScoreAvg30d - riskScoreAvg7d }}",
        "isTrusted": "{{ deviceAge > 30 && riskScoreAvg30d < 25 }}"
      }
    }
  ]
}

Best Practices

  • Keep claims minimal: Only include data needed for authorization decisions
  • Cache external data: Use TTL caching for external API calls
  • Version your claims: Include a version field for backward compatibility
  • Validate on server: Always verify claims server-side, never trust client-only validation
  • Monitor claim usage: Track which claims are used to optimize your rules
  • Test thoroughly: Use staging environments to test claim logic

Limitations by Tier

Tier Max Claims Claim Types External API
Basic 0 - -
Professional 5 Static, Conditional No
Enterprise 20 All types Yes
Enterprise Plus 100 All types + Custom Yes + Webhooks

Next Steps