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:
- Workspace: "Acme Corp" (your company)
- App: "Acme Mobile App" (your customer-facing app)
- App Users: Your end customers (john@customer.com, jane@customer.com)
- Users: Your team members (admin@acme.com, dev@acme.com)
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
Claim | Description | Example |
---|---|---|
uuid | App User ID - identifies the authenticated user | "usr_1234abcd" |
aid | App ID - your application identifier | "app_5678efgh" |
uid | Session ID - unique identifier for this login session | "sess_9012ijkl" |
wid | Workspace ID - your organization's identifier | "ws_3456mnop" |
crid | Credential ID - method used to authenticate (WebAuthn, etc.) | "cred_7890qrst" |
exp | Expiration - when the token expires | 1640995200 |
iat | Issued At - when the token was created | 1640991600 |
Offline JWT Verification (Recommended)
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 };
}
Step 3: Using JWKS (Recommended for Production)
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
-
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 -
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');
} -
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
Method | Speed | Data Available | Internet Required | Rate Limits |
---|---|---|---|---|
Offline | ⚡ Fast | Basic claims only | ❌ No | ❌ None |
Online | 🐌 Slower | Full user data | ✅ Yes | ✅ Standard API limits |
Best Practices
Security Recommendations
- Use HTTPS Only: Never transmit tokens over HTTP
- Validate Audience: Always check the
aid
claim matches your app - Check Expiration: Validate
exp
claim before processing - Store Securely: Use secure storage for tokens on client-side
Performance Optimization
- Cache Public Keys: Refresh every 1-6 hours
- Use Offline Verification: For high-traffic applications
- Connection Pooling: Reuse HTTP connections for online verification
- 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