Custom Claims
Quick Start
Advanced Features
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 |