Skip to main content

JWT

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