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