Skip to main content
A comprehensive guide for validating Scute authentication tokens in your application.

Understanding Scute’s Structure

The Basics

Scute organizes users and permissions in a simple hierarchy:
Workspace (Your Organization)
  ├── Apps (Your Applications) 
  │   └── App Users (Your End Users)
  └── Users (Workspace Members)
Example:

Key Concepts

  • Workspace: Your organization’s account on Scute
  • App: Each application you build (mobile app, web app, etc.)
  • App Users: Your end customers who log into your apps
  • Users: Your team members who manage the workspace

JWT Token Structure

When a user successfully authenticates, Scute issues a JWT token with the following structure:
{
  "uuid": "app_user_id",       // The authenticated user ID
  "aid": "your_app_id",        // Your application ID
  "uid": "session_id",         // Unique session identifier
  "wid": "workspace_id",       // Your workspace ID
  "crid": "credential_id",     // Webauthn credential id (optional)
  "exp": 1640995200,           // Token expiration (Unix timestamp)
  "iat": 1640991600            // Token issued at (Unix timestamp)
}

Token Claims Explained

ClaimDescriptionExample
uuidApp User ID - identifies the authenticated user"usr_1234abcd"
aidApp ID - your application identifier"app_5678efgh"
uidSession ID - unique identifier for this login session"sess_9012ijkl"
widWorkspace ID - your organization’s identifier"ws_3456mnop"
cridCredential ID - method used to authenticate (WebAuthn, etc.)"cred_7890qrst"
expExpiration - when the token expires1640995200
iatIssued At - when the token was created1640991600
Offline verification is faster and more scalable as it doesn’t require API calls to validate tokens.

Step 1: Get the Public Key

Fetch your app’s public key for JWT verification (this is a public endpoint):
// Get RSA public key in PEM format
const response = await fetch(`https://api.scute.io/v1/auth/${app_id}/public_key`);
const { public_key, algorithm, key_id } = await response.json();

console.log(public_key);
// -----BEGIN PUBLIC KEY-----
// MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
// -----END PUBLIC KEY-----
Response Format:
{
  "public_key": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG...\n-----END PUBLIC KEY-----",
  "algorithm": "RS256",
  "key_id": "ws_1234_1640991600"
}

Step 2: Offline JWT Verification

import jwt from 'jsonwebtoken';

// Your session token from user login
const accessToken = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...";

try {
  // Verify the token offline
  const decoded = jwt.verify(accessToken, public_key, {
    algorithms: ['RS256']
    // Note: Current tokens don't include standard 'aud' or 'iss' claims
    // Audience validation is done manually below using the 'aid' claim
  });
  
  // Manually validate that token is for your app
  if (decoded.aid !== your_app_id) {
    throw new Error('Token not intended for this app');
  }
  
  console.log('✅ Token is valid!');
  console.log('User ID:', decoded.uuid);
  console.log('App ID:', decoded.aid);
  console.log('Session ID:', decoded.uid);
  
  // Token is valid - user is authenticated
  return {
    authenticated: true,
    userId: decoded.uuid,
    sessionId: decoded.uid,
    expiresAt: new Date(decoded.exp * 1000)
  };
  
} catch (error) {
  console.log('❌ Token is invalid:', error.message);
  return { authenticated: false, error: error.message };
}
JWKS (JSON Web Key Set) provides automatic key rotation support:
import jwksClient from 'jwks-rsa';

// Create JWKS client (handles caching and key rotation)
const client = jwksClient({
  jwksUri: `https://api.scute.io/v1/auth/${app_id}/jwks`,
  cache: true,
  cacheMaxAge: 3600000, // Cache for 1 hour
  rateLimit: true,
  jwksRequestsPerMinute: 5
});

// Get signing key function
function getKey(header, callback) {
  client.getSigningKey(header.kid, (err, key) => {
    if (err) {
      callback(err);
      return;
    }
    const signingKey = key.getPublicKey();
    callback(null, signingKey);
  });
}

// Verify token with automatic key management
jwt.verify(accessToken, getKey, {
  algorithms: ['RS256']
}, (err, decoded) => {
  if (err) {
    console.log('❌ Token invalid:', err.message);
    return;
  }
  
  console.log('✅ Token valid:', decoded);
  // Proceed with authenticated user
});

Key Points for Offline Validation

✅ Benefits

  • 🚀 Performance: No API calls required for each validation
  • 🔒 Security: Cryptographically secure verification
  • ⚡ Scalability: No rate limits on token validation
  • 🌐 Offline Support: Works without internet connectivity

⚠️ Important Considerations

  1. Cache Public Keys Wisely
    // Good: Cache for reasonable time
    const cacheTime = 1000 * 60 * 60; // 1 hour
    
    // Bad: Cache forever (keys may rotate)
    // Don't cache indefinitely
    
  2. Validate Token Claims
    // Ensure token is for your app (manual audience validation)
    if (decoded.aid !== your_app_id) {
      throw new Error('Token not intended for this app');
    }
    
    // Verify token hasn't expired
    if (decoded.exp < Math.floor(Date.now() / 1000)) {
      throw new Error('Token has expired');
    }
    
  3. Handle Key Rotation
    // Use JWKS for automatic key rotation
    // or refresh public keys every hour
    

Online Verification (Alternative)

If you prefer server-side validation or need additional user data, use online verification:

Endpoint

GET https://api.scute.io/v1/auth/{app_id}/current_user
Authorization: Bearer {access_token}

Example Implementation

async function verifyTokenOnline(accessToken, appId) {
  try {
    const response = await fetch(`https://api.scute.io/v1/auth/${appId}/current_user`, {
      method: 'GET',
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Content-Type': 'application/json'
      }
    });
    
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
    
    const userData = await response.json();
    
    return {
      authenticated: true,
      user: userData.user,
      // Additional user data available:
      // - email, phone, name
      // - verification status
      // - user metadata
    };
    
  } catch (error) {
    console.log('❌ Online verification failed:', error.message);
    return { 
      authenticated: false, 
      error: error.message 
    };
  }
}

// Usage
const result = await verifyTokenOnline(accessToken, 'your_app_id');
if (result.authenticated) {
  console.log('User data:', result.user);
} else {
  console.log('Authentication failed:', result.error);
}

Online vs Offline Verification

MethodSpeedData AvailableInternet RequiredRate Limits
Offline⚡ FastBasic claims only❌ No❌ None
Online🐌 SlowerFull user data✅ Yes✅ Standard API limits

Best Practices

Security Recommendations

  1. Use HTTPS Only: Never transmit tokens over HTTP
  2. Validate Audience: Always check the aid claim matches your app
  3. Check Expiration: Validate exp claim before processing
  4. Store Securely: Use secure storage for tokens on client-side

Performance Optimization

  1. Cache Public Keys: Refresh every 1-6 hours
  2. Use Offline Verification: For high-traffic applications
  3. Connection Pooling: Reuse HTTP connections for online verification
  4. Error Handling: Gracefully handle network failures

Example: Complete Verification Function

class ScuteJWTVerifier {
  constructor(appId) {
    this.appId = appId;
    this.publicKey = null;
    this.keyLastFetched = null;
    this.cacheTime = 3600000; // 1 hour
  }
  
  async getPublicKey() {
    const now = Date.now();
    
    // Use cached key if still valid
    if (this.publicKey && this.keyLastFetched && 
        (now - this.keyLastFetched) < this.cacheTime) {
      return this.publicKey;
    }
    
    // Fetch fresh public key
    const response = await fetch(`https://api.scute.io/v1/auth/${this.appId}/public_key`);
    const data = await response.json();
    
    this.publicKey = data.public_key;
    this.keyLastFetched = now;
    
    return this.publicKey;
  }
  
  async verifyToken(token) {
    try {
      const publicKey = await this.getPublicKey();
      
      const decoded = jwt.verify(token, publicKey, {
        algorithms: ['RS256']
      });
      
      // Manually validate audience (app ID)
      if (decoded.aid !== this.appId) {
        throw new Error('Token not intended for this app');
      }
      
      return {
        valid: true,
        userId: decoded.uuid,
        sessionId: decoded.uid,
        expiresAt: new Date(decoded.exp * 1000)
      };
      
    } catch (error) {
      return {
        valid: false,
        error: error.message
      };
    }
  }
}

// Usage
const verifier = new ScuteJWTVerifier('your_app_id');
const result = await verifier.verifyToken(userToken);

if (result.valid) {
  console.log(`User ${result.userId} is authenticated`);
} else {
  console.log(`Authentication failed: ${result.error}`);
}

Troubleshooting

Common Issues

“Invalid signature” Error
  • Check that you’re using the correct public key for your app
  • Ensure you’re using the RS256 algorithm
  • Verify the token hasn’t been modified
“Token expired” Error
  • Check system clock synchronization
  • Token has a limited lifetime (typically 1 hour)
  • Request a new token using refresh flow
“Invalid audience” Error
  • Ensure the aid claim matches your app ID
  • Token might be intended for a different app
Network Errors (Online Verification)
  • Check API endpoint URL is correct
  • Verify internet connectivity
  • Ensure proper Authorization header format