Skip to Content

REST API Security Token Handler

Comprehensive security handler for REST APIs with JWT token validation, rate limiting, IP whitelisting, and audit logging.

#script-include #rest-api #security #jwt #rate-limiting #authentication #audit #authorization

Script Code

JavaScript
1var RESTSecurityHandler = Class.create();
2RESTSecurityHandler.prototype = {
3    initialize: function() {
4        this.rateLimitTable = 'u_api_rate_limit';
5        this.auditTable = 'u_api_audit_log';
6        this.jwtSecret = gs.getProperty('api.jwt.secret', 'default-secret-change-me');
7        this.defaultRateLimit = 100; // requests per hour
8        this.tokenExpireHours = 24;
9    },
10
11    /**
12     * Validate incoming REST API request
13     * @param {Object} request - REST request object
14     * @param {Object} response - REST response object  
15     * @param {Object} config - Security configuration
16     * @returns {Object} Validation result with user info
17     */
18    validateRequest: function(request, response, config) {
19        try {
20            var validationResult = {
21                isValid: false,
22                user: null,
23                message: '',
24                rateLimitRemaining: 0,
25                auditId: null
26            };
27
28            config = config || {};
29            var clientIP = this.getClientIP(request);
30            var endpoint = request.pathInfo;
31            var method = request.method;
32            var userAgent = request.getHeader('User-Agent') || '';
33            var timestamp = new GlideDateTime();
34
35            // 1. IP Whitelist Check
36            if (config.ipWhitelist && config.ipWhitelist.length > 0) {
37                if (!this.isIPWhitelisted(clientIP, config.ipWhitelist)) {
38                    validationResult.message = 'IP address not authorized';
39                    this.logAuditEvent(clientIP, endpoint, method, 'IP_BLOCKED', '', userAgent);
40                    response.setStatus(403);
41                    return validationResult;
42                }
43            }
44
45            // 2. Authentication Check
46            var authResult = this.authenticateRequest(request, config);
47            if (!authResult.isValid) {
48                validationResult.message = authResult.message;
49                this.logAuditEvent(clientIP, endpoint, method, 'AUTH_FAILED', authResult.user, userAgent);
50                response.setStatus(401);
51                return validationResult;
52            }
53
54            validationResult.user = authResult.user;
55
56            // 3. Rate Limiting
57            var rateLimitResult = this.checkRateLimit(clientIP, authResult.user, endpoint, config);
58            if (!rateLimitResult.allowed) {
59                validationResult.message = 'Rate limit exceeded';
60                validationResult.rateLimitRemaining = rateLimitResult.remaining;
61                this.logAuditEvent(clientIP, endpoint, method, 'RATE_LIMIT', authResult.user, userAgent);
62                response.setStatus(429);
63                response.setHeader('Retry-After', '3600'); // 1 hour
64                return validationResult;
65            }
66
67            validationResult.rateLimitRemaining = rateLimitResult.remaining;
68
69            // 4. Authorization Check (role/permission based)
70            if (config.requiredRoles || config.requiredPermissions) {
71                var authzResult = this.authorizeUser(authResult.user, config);
72                if (!authzResult.authorized) {
73                    validationResult.message = 'Insufficient permissions';
74                    this.logAuditEvent(clientIP, endpoint, method, 'AUTHZ_FAILED', authResult.user, userAgent);
75                    response.setStatus(403);
76                    return validationResult;
77                }
78            }
79
80            // 5. Request Validation (payload, content-type, etc.)
81            if (config.validatePayload) {
82                var payloadResult = this.validatePayload(request, config);
83                if (!payloadResult.valid) {
84                    validationResult.message = payloadResult.message;
85                    this.logAuditEvent(clientIP, endpoint, method, 'PAYLOAD_INVALID', authResult.user, userAgent);
86                    response.setStatus(400);
87                    return validationResult;
88                }
89            }
90
91            // All checks passed
92            validationResult.isValid = true;
93            validationResult.message = 'Request validated successfully';
94            validationResult.auditId = this.logAuditEvent(clientIP, endpoint, method, 'SUCCESS', authResult.user, userAgent);
95
96            // Set response headers
97            response.setHeader('X-RateLimit-Remaining', validationResult.rateLimitRemaining.toString());
98            response.setHeader('X-Request-ID', validationResult.auditId);
99
100            return validationResult;
101
102        } catch (e) {
103            gs.error('RESTSecurityHandler.validateRequest error: ' + e.message);
104            response.setStatus(500);
105            return {
106                isValid: false,
107                message: 'Internal security validation error',
108                user: null,
109                rateLimitRemaining: 0,
110                auditId: null
111            };
112        }
113    },
114
115    /**
116     * Authenticate request using various methods
117     * @param {Object} request - REST request
118     * @param {Object} config - Auth configuration
119     * @returns {Object} Authentication result
120     */
121    authenticateRequest: function(request, config) {
122        var authMethod = config.authMethod || 'jwt';
123        var result = { isValid: false, user: null, message: '' };
124
125        try {
126            switch (authMethod) {
127                case 'jwt':
128                    result = this.validateJWTToken(request);
129                    break;
130                case 'basic':
131                    result = this.validateBasicAuth(request);
132                    break;
133                case 'api_key':
134                    result = this.validateAPIKey(request);
135                    break;
136                case 'oauth':
137                    result = this.validateOAuthToken(request);
138                    break;
139                default:
140                    result.message = 'Unsupported authentication method';
141            }
142        } catch (e) {
143            result.message = 'Authentication error: ' + e.message;
144        }
145
146        return result;
147    },
148
149    /**
150     * Validate JWT token
151     * @param {Object} request - REST request
152     * @returns {Object} JWT validation result
153     */
154    validateJWTToken: function(request) {
155        var authHeader = request.getHeader('Authorization');
156        var result = { isValid: false, user: null, message: '' };
157
158        if (!authHeader || !authHeader.startsWith('Bearer ')) {
159            result.message = 'Missing or invalid Authorization header';
160            return result;
161        }
162
163        var token = authHeader.substring(7); // Remove 'Bearer '
164
165        try {
166            // Simple JWT validation (in production, use proper JWT library)
167            var payload = this.decodeJWTPayload(token);
168            
169            if (!payload) {
170                result.message = 'Invalid JWT token format';
171                return result;
172            }
173
174            // Check expiration
175            var now = Math.floor(Date.now() / 1000);
176            if (payload.exp && payload.exp < now) {
177                result.message = 'JWT token expired';
178                return result;
179            }
180
181            // Validate signature (simplified)
182            if (!this.verifyJWTSignature(token, this.jwtSecret)) {
183                result.message = 'Invalid JWT signature';
184                return result;
185            }
186
187            // Look up user
188            var user = this.getUserByIdentifier(payload.sub || payload.username);
189            if (!user) {
190                result.message = 'User not found';
191                return result;
192            }
193
194            result.isValid = true;
195            result.user = user;
196            result.message = 'JWT token validated';
197
198        } catch (e) {
199            result.message = 'JWT validation error: ' + e.message;
200        }
201
202        return result;
203    },
204
205    /**
206     * Validate API Key authentication
207     * @param {Object} request - REST request
208     * @returns {Object} API key validation result
209     */
210    validateAPIKey: function(request) {
211        var apiKey = request.getHeader('X-API-Key') || request.getQueryParameter('api_key');
212        var result = { isValid: false, user: null, message: '' };
213
214        if (!apiKey) {
215            result.message = 'Missing API key';
216            return result;
217        }
218
219        try {
220            // Look up API key in custom table
221            var grApiKey = new GlideRecord('u_api_keys');
222            grApiKey.addQuery('api_key', apiKey);
223            grApiKey.addQuery('active', true);
224            grApiKey.addQuery('expires_on', '>=', new GlideDateTime()); // Not expired
225            grApiKey.setLimit(1);
226            grApiKey.query();
227
228            if (grApiKey.next()) {
229                var user = this.getUserByID(grApiKey.user.toString());
230                if (user) {
231                    result.isValid = true;
232                    result.user = user;
233                    result.message = 'API key validated';
234
235                    // Update last used timestamp
236                    grApiKey.last_used = new GlideDateTime();
237                    grApiKey.update();
238                } else {
239                    result.message = 'API key user not found';
240                }
241            } else {
242                result.message = 'Invalid or expired API key';
243            }
244
245        } catch (e) {
246            result.message = 'API key validation error: ' + e.message;
247        }
248
249        return result;
250    },
251
252    /**
253     * Check rate limits for user/IP
254     * @param {String} clientIP - Client IP address
255     * @param {String} user - User ID
256     * @param {String} endpoint - API endpoint
257     * @param {Object} config - Rate limit configuration
258     * @returns {Object} Rate limit result
259     */
260    checkRateLimit: function(clientIP, user, endpoint, config) {
261        var result = { allowed: true, remaining: this.defaultRateLimit };
262        
263        try {
264            var rateLimit = config.rateLimit || this.defaultRateLimit;
265            var windowHours = config.rateLimitWindow || 1;
266            var rateLimitKey = user || clientIP;
267            
268            // Calculate window start time
269            var windowStart = new GlideDateTime();
270            windowStart.addSeconds(-(windowHours * 3600));
271
272            // Check existing rate limit record
273            var grRateLimit = new GlideRecord(this.rateLimitTable);
274            grRateLimit.addQuery('identifier', rateLimitKey);
275            grRateLimit.addQuery('endpoint', endpoint);
276            grRateLimit.addQuery('window_start', '>=', windowStart);
277            grRateLimit.setLimit(1);
278            grRateLimit.query();
279
280            if (grRateLimit.next()) {
281                var currentCount = parseInt(grRateLimit.request_count) || 0;
282                
283                if (currentCount >= rateLimit) {
284                    result.allowed = false;
285                    result.remaining = 0;
286                } else {
287                    // Increment count
288                    grRateLimit.request_count = currentCount + 1;
289                    grRateLimit.last_request = new GlideDateTime();
290                    grRateLimit.update();
291                    result.remaining = rateLimit - (currentCount + 1);
292                }
293            } else {
294                // Create new rate limit record
295                var grNew = new GlideRecord(this.rateLimitTable);
296                grNew.initialize();
297                grNew.identifier = rateLimitKey;
298                grNew.endpoint = endpoint;
299                grNew.request_count = 1;
300                grNew.rate_limit = rateLimit;
301                grNew.window_start = new GlideDateTime();
302                grNew.last_request = new GlideDateTime();
303                grNew.insert();
304                
305                result.remaining = rateLimit - 1;
306            }
307
308        } catch (e) {
309            gs.error('Rate limit check error: ' + e.message);
310            // Allow request on error to avoid blocking legitimate traffic
311        }
312
313        return result;
314    },
315
316    /**
317     * Check user authorization
318     * @param {String} user - User ID
319     * @param {Object} config - Authorization configuration
320     * @returns {Object} Authorization result
321     */
322    authorizeUser: function(user, config) {
323        var result = { authorized: false, message: '' };
324
325        try {
326            var grUser = new GlideRecord('sys_user');
327            if (!grUser.get(user)) {
328                result.message = 'User not found';
329                return result;
330            }
331
332            // Check required roles
333            if (config.requiredRoles && config.requiredRoles.length > 0) {
334                var hasRequiredRole = false;
335                
336                for (var i = 0; i < config.requiredRoles.length; i++) {
337                    if (grUser.hasRole(config.requiredRoles[i])) {
338                        hasRequiredRole = true;
339                        break;
340                    }
341                }
342
343                if (!hasRequiredRole) {
344                    result.message = 'User lacks required roles: ' + config.requiredRoles.join(', ');
345                    return result;
346                }
347            }
348
349            // Check specific permissions (custom logic)
350            if (config.requiredPermissions) {
351                for (var j = 0; j < config.requiredPermissions.length; j++) {
352                    var permission = config.requiredPermissions[j];
353                    if (!this.userHasPermission(user, permission)) {
354                        result.message = 'User lacks permission: ' + permission;
355                        return result;
356                    }
357                }
358            }
359
360            result.authorized = true;
361            result.message = 'User authorized';
362
363        } catch (e) {
364            result.message = 'Authorization check error: ' + e.message;
365        }
366
367        return result;
368    },
369
370    /**
371     * Log API audit event
372     * @param {String} clientIP - Client IP
373     * @param {String} endpoint - API endpoint
374     * @param {String} method - HTTP method
375     * @param {String} status - Request status
376     * @param {String} user - User ID
377     * @param {String} userAgent - User agent string
378     * @returns {String} Audit record sys_id
379     */
380    logAuditEvent: function(clientIP, endpoint, method, status, user, userAgent) {
381        try {
382            var grAudit = new GlideRecord(this.auditTable);
383            grAudit.initialize();
384            grAudit.client_ip = clientIP;
385            grAudit.endpoint = endpoint;
386            grAudit.http_method = method;
387            grAudit.status = status;
388            grAudit.user = user || '';
389            grAudit.user_agent = userAgent;
390            grAudit.timestamp = new GlideDateTime();
391            
392            return grAudit.insert();
393            
394        } catch (e) {
395            gs.error('Audit logging error: ' + e.message);
396            return null;
397        }
398    },
399
400    /**
401     * Get client IP address from request
402     * @param {Object} request - REST request
403     * @returns {String} Client IP address
404     */
405    getClientIP: function(request) {
406        // Check various headers for real IP
407        var xForwardedFor = request.getHeader('X-Forwarded-For');
408        var xRealIP = request.getHeader('X-Real-IP');
409        var cfConnectingIP = request.getHeader('CF-Connecting-IP');
410        
411        if (xForwardedFor) {
412            return xForwardedFor.split(',')[0].trim();
413        }
414        if (xRealIP) {
415            return xRealIP;
416        }
417        if (cfConnectingIP) {
418            return cfConnectingIP;
419        }
420        
421        return request.getRemoteAddr() || 'unknown';
422    },
423
424    /**
425     * Check if IP is in whitelist
426     * @param {String} clientIP - Client IP to check
427     * @param {Array} whitelist - Array of allowed IPs/CIDRs
428     * @returns {Boolean} True if IP is whitelisted
429     */
430    isIPWhitelisted: function(clientIP, whitelist) {
431        for (var i = 0; i < whitelist.length; i++) {
432            var allowedIP = whitelist[i];
433            
434            // Simple IP match
435            if (clientIP === allowedIP) {
436                return true;
437            }
438            
439            // CIDR range check (simplified - implement proper CIDR logic)
440            if (allowedIP.indexOf('/') > -1) {
441                // Basic subnet check implementation
442                if (this.isIPInSubnet(clientIP, allowedIP)) {
443                    return true;
444                }
445            }
446        }
447        
448        return false;
449    },
450
451    /**
452     * Simplified CIDR subnet check
453     * @param {String} ip - IP address to check
454     * @param {String} cidr - CIDR notation (e.g., '192.168.1.0/24')
455     * @returns {Boolean} True if IP is in subnet
456     */
457    isIPInSubnet: function(ip, cidr) {
458        // Simplified implementation - use proper CIDR library in production
459        var parts = cidr.split('/');
460        var subnet = parts[0];
461        var prefix = parseInt(parts[1]);
462        
463        // Basic check - implement proper binary subnet matching
464        return ip.startsWith(subnet.substring(0, subnet.lastIndexOf('.') + 1));
465    },
466
467    /**
468     * Generate JWT token for user
469     * @param {String} userId - User sys_id
470     * @param {Object} claims - Additional claims
471     * @returns {String} JWT token
472     */
473    generateJWTToken: function(userId, claims) {
474        try {
475            claims = claims || {};
476            
477            var header = {
478                "alg": "HS256",
479                "typ": "JWT"
480            };
481            
482            var now = Math.floor(Date.now() / 1000);
483            var payload = {
484                "sub": userId,
485                "iat": now,
486                "exp": now + (this.tokenExpireHours * 3600),
487                "iss": "servicenow-api"
488            };
489            
490            // Add custom claims
491            for (var key in claims) {
492                payload[key] = claims[key];
493            }
494            
495            // Simple JWT creation (use proper JWT library in production)
496            var encodedHeader = GlideStringUtil.base64Encode(JSON.stringify(header));
497            var encodedPayload = GlideStringUtil.base64Encode(JSON.stringify(payload));
498            var signature = this.createJWTSignature(encodedHeader + '.' + encodedPayload);
499            
500            return encodedHeader + '.' + encodedPayload + '.' + signature;
501            
502        } catch (e) {
503            gs.error('JWT generation error: ' + e.message);
504            return null;
505        }
506    },
507
508    /**
509     * Helper functions (implement based on your needs)
510     */
511    decodeJWTPayload: function(token) {
512        try {
513            var parts = token.split('.');
514            if (parts.length !== 3) return null;
515            
516            var payload = GlideStringUtil.base64Decode(parts[1]);
517            return JSON.parse(payload);
518        } catch (e) {
519            return null;
520        }
521    },
522
523    verifyJWTSignature: function(token, secret) {
524        // Implement proper HMAC-SHA256 signature verification
525        return true; // Simplified for example
526    },
527
528    createJWTSignature: function(data) {
529        // Implement proper HMAC-SHA256 signature creation
530        return GlideStringUtil.base64Encode('signature');
531    },
532
533    getUserByIdentifier: function(identifier) {
534        var grUser = new GlideRecord('sys_user');
535        grUser.addQuery('user_name', identifier);
536        grUser.addQuery('active', true);
537        grUser.setLimit(1);
538        grUser.query();
539        
540        return grUser.next() ? grUser.sys_id.toString() : null;
541    },
542
543    getUserByID: function(userId) {
544        var grUser = new GlideRecord('sys_user');
545        if (grUser.get(userId) && grUser.active) {
546            return userId;
547        }
548        return null;
549    },
550
551    userHasPermission: function(userId, permission) {
552        // Implement custom permission check logic
553        return true;
554    },
555
556    validatePayload: function(request, config) {
557        // Implement payload validation based on config
558        return { valid: true, message: '' };
559    },
560
561    validateBasicAuth: function(request) {
562        // Implement Basic Authentication validation
563        return { isValid: false, user: null, message: 'Basic auth not implemented' };
564    },
565
566    validateOAuthToken: function(request) {
567        // Implement OAuth token validation
568        return { isValid: false, user: null, message: 'OAuth not implemented' };
569    },
570
571    type: 'RESTSecurityHandler'
572};

How to Use

1. Create Script Include with Name 'RESTSecurityHandler'\n2. Set API Name to 'RESTSecurityHandler' and check 'Accessible from'\n3. Create supporting tables:\n - u_api_rate_limit: identifier, endpoint, request_count, rate_limit, window_start, last_request\n - u_api_audit_log: client_ip, endpoint, http_method, status, user, user_agent, timestamp\n - u_api_keys: api_key, user (reference), active, expires_on, last_used\n4. Usage in REST API:\n var security = new RESTSecurityHandler();\n var config = { authMethod: 'jwt', rateLimit: 100, requiredRoles: ['api_user'] };\n var result = security.validateRequest(request, response, config);\n if (!result.isValid) { return; }\n5. Set system property 'api.jwt.secret' with secure secret\n6. Customize IP whitelisting, rate limits, and validation rules\n7. Implement proper JWT library for production use

Explore More Scripts

Browse our complete library of ServiceNow scripts

View All Scripts