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/.well-known/jwks.json"
// Issuer and audience are per-organization. Decode a real token to read them.
const (
issuer = "app-attest-acme"
audience = "Acme"
)
type GrantivaClaims struct {
RiskScore *int `json:"risk_score"` // nil when absent (Free plan)
DeviceIntegrity string `json:"device_integrity"`
JailbreakDetected bool `json:"jailbreak_detected"`
AttestationCount int `json:"attestation_count"`
BundleID string `json:"bundle_id"`
TeamID string `json:"team_id"`
CustomClaims map[string]any `json:"custom_claims"`
jwt.RegisteredClaims // Subject holds the App Attest key id
}
// 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(issuer),
jwt.WithAudience(audience),
jwt.WithValidMethods([]string{"RS256"}),
)
if err != nil || !token.Valid {
return nil, fmt.Errorf("invalid token: %w", err)
}
// Block high-risk devices. A nil score means no signed signal, not "safe".
if claims.JailbreakDetected || (claims.RiskScore != nil && *claims.RiskScore > 75) {
return nil, errors.New("device blocked due to high risk")
}
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))
})
}