Skip to main content
After your iOS app calls grantiva.validateAttestation(), it receives a signed JWT. Your backend should verify this token on every authenticated request to confirm the device passed attestation and to read device intelligence claims.

How it works

  1. Your app sends the JWT as a Bearer token in the Authorization header
  2. Your backend fetches Grantiva’s public key from /api/v1/attestation/public-key
  3. You verify the JWT signature and expiry locally (no network call per request)
  4. You extract device claims: risk_score, risk_category, jailbreak status, and any custom claims

JWT Claims Reference

ClaimTypeDescription
device_idstringUnique device identifier
risk_scoreinteger|null0–100 device risk score. null on Free tier
risk_categorystring"trusted" (0–20), "suspicious" (21–75), or "blocked" (76–100)
device_integritystringIntegrity status string from Apple’s attestation
jailbreak_detectedbooleanWhether jailbreak indicators were found
attestation_countintegerTotal attestations from this device
custom_claimsobjectYour custom JWT claims configured in the dashboard
expintegerStandard JWT expiry (Unix timestamp)
issstring"grantiva"

Node.js

Uses jose (ESM/CJS, works in Node.js and edge runtimes).
npm install jose
import { createRemoteJWKSet, jwtVerify } from 'jose'

const JWKS = createRemoteJWKSet(
  new URL('https://api.grantiva.io/api/v1/attestation/public-key')
)

async function verifyGrantivaToken(token) {
  const { payload } = await jwtVerify(token, JWKS, {
    issuer: 'grantiva',
    algorithms: ['RS256'],
  })

  const riskScore    = payload.risk_score       // number | null
  const riskCategory = payload.risk_category    // "trusted" | "suspicious" | "blocked"
  const jailbreak    = payload.jailbreak_detected // boolean
  const customClaims = payload.custom_claims    // object | undefined

  // Block high-risk devices
  if (riskCategory === 'blocked') {
    throw new Error('Device blocked due to high risk score')
  }

  return payload
}

// Express middleware example
async function requireAttestedDevice(req, res, next) {
  const token = req.headers.authorization?.replace('Bearer ', '')
  if (!token) return res.status(401).json({ error: 'Missing token' })

  try {
    req.deviceClaims = await verifyGrantivaToken(token)
    next()
  } catch (err) {
    res.status(401).json({ error: 'Invalid or expired device token' })
  }
}

Python

Uses PyJWT with cryptography for RS256.
pip install PyJWT[crypto] requests
import requests
import jwt
from jwt import PyJWKClient

JWKS_URL = "https://api.grantiva.io/api/v1/attestation/public-key"

# Reuse the client across requests — it caches the public key
jwks_client = PyJWKClient(JWKS_URL)

def verify_grantiva_token(token: str) -> dict:
    signing_key = jwks_client.get_signing_key_from_jwt(token)

    payload = jwt.decode(
        token,
        signing_key.key,
        algorithms=["RS256"],
        issuer="grantiva",
    )

    risk_score    = payload.get("risk_score")       # int | None
    risk_category = payload.get("risk_category")    # "trusted" | "suspicious" | "blocked"
    jailbreak     = payload.get("jailbreak_detected", False)
    custom_claims = payload.get("custom_claims", {})

    # Block high-risk devices
    if risk_category == "blocked":
        raise ValueError(f"Device blocked: risk_score={risk_score}")

    return payload

# FastAPI / Flask decorator example
from functools import wraps
from flask import request, jsonify, g

def require_attested_device(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        token = request.headers.get("Authorization", "").removeprefix("Bearer ")
        if not token:
            return jsonify({"error": "Missing token"}), 401
        try:
            g.device_claims = verify_grantiva_token(token)
        except Exception as e:
            return jsonify({"error": str(e)}), 401
        return f(*args, **kwargs)
    return decorated

Go

Uses golang-jwt/jwt with JWKS fetching via MicahParks/keyfunc.
go get github.com/golang-jwt/jwt/v5
go get github.com/MicahParks/keyfunc/v3
package main

import (
    "context"
    "errors"
    "fmt"
    "net/http"
    "strings"

    "github.com/MicahParks/keyfunc/v3"
    "github.com/golang-jwt/jwt/v5"
)

const jwksURL = "https://api.grantiva.io/api/v1/attestation/public-key"

type GrantivaClaims struct {
    DeviceID          string         `json:"device_id"`
    RiskScore         *int           `json:"risk_score"`        // nil on Free tier
    RiskCategory      string         `json:"risk_category"`
    DeviceIntegrity   string         `json:"device_integrity"`
    JailbreakDetected bool           `json:"jailbreak_detected"`
    AttestationCount  int            `json:"attestation_count"`
    CustomClaims      map[string]any `json:"custom_claims"`
    jwt.RegisteredClaims
}

// Initialize once at startup; keyfunc handles key rotation automatically
var jwks keyfunc.Keyfunc

func init() {
    var err error
    jwks, err = keyfunc.NewDefault([]string{jwksURL})
    if err != nil {
        panic(fmt.Sprintf("failed to fetch JWKS: %v", err))
    }
}

func VerifyGrantivaToken(tokenStr string) (*GrantivaClaims, error) {
    claims := &GrantivaClaims{}

    token, err := jwt.ParseWithClaims(tokenStr, claims, jwks.Keyfunc,
        jwt.WithIssuedAt(),
        jwt.WithIssuer("grantiva"),
        jwt.WithValidMethods([]string{"RS256"}),
    )
    if err != nil || !token.Valid {
        return nil, fmt.Errorf("invalid token: %w", err)
    }

    // Block high-risk devices
    if claims.RiskCategory == "blocked" {
        return nil, errors.New("device blocked due to high risk score")
    }

    return claims, nil
}

// HTTP middleware example
func RequireAttestedDevice(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        auth := r.Header.Get("Authorization")
        token := strings.TrimPrefix(auth, "Bearer ")
        if token == "" {
            http.Error(w, "missing token", http.StatusUnauthorized)
            return
        }

        claims, err := VerifyGrantivaToken(token)
        if err != nil {
            http.Error(w, err.Error(), http.StatusUnauthorized)
            return
        }

        ctx := context.WithValue(r.Context(), "device_claims", claims)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

Reading custom claims

Custom claims you configure in the Grantiva dashboard appear in the custom_claims object:
// Node.js
const userTier = payload.custom_claims?.user_tier   // "premium"
const region   = payload.custom_claims?.region      // "us-east"
# Python
user_tier = payload.get("custom_claims", {}).get("user_tier")
// Go
userTier, _ := claims.CustomClaims["user_tier"].(string)

Risk-based access control

// Allow, warn, or block based on risk category
switch (payload.risk_category) {
  case 'trusted':     // riskScore 0–20
    break             // full access
  case 'suspicious':  // riskScore 21–75
    logSuspiciousDevice(payload.device_id)
    break             // allow but log
  case 'blocked':     // riskScore 76–100
    return res.status(403).json({ error: 'Device blocked' })
}

Caching the public key

All three examples cache the JWKS response and refresh it automatically when key rotation occurs. Do not fetch the public key on every request — it adds latency and will trigger rate limiting.