UNPKG

volos-oauth-common

Version:

Support for OAuth providers in the Volos system.

276 lines (250 loc) 8.35 kB
'use strict'; /* cache: token (as JSON) -> time added (in ms) */ var _ = require('underscore'); var debug = require('debug')('oauth'); var Url = require('url'); var create = function(cache, target) { return new OAuthCache(cache, target); }; module.exports.create = create; var OAuthCache = function(cache, target) { if (cache.options.encoding) { throw new Error('Cache must not specify an encoding option.'); } this.cache = cache; this.target = target; }; OAuthCache.prototype.cacheToken = function(err, token, cb) { try { if (err) { return cb(err); } var result = token; if (typeof token === 'string') { token = Url.parse(token); } if (!token.access_token) { return cb(err, result); } var key = token.access_token; token.cached_at = new Date().getTime(); var target = JSON.stringify(token); if (debug.enabled) { debug('cache token: ' + target); } var opts = null; if (token.expires_in) { var ttl = token.expires_in * 1000; // expires_in is seconds, ttl is ms opts = { ttl: Math.min(ttl, this.cache.options.ttl) }; // max upper limit at this.cache.options.ttl } this.cache.set(key, target, opts); cb(null, result); } catch (err) { debug('err: ' + err); cb(err); } }; OAuthCache.prototype.getCachedToken = function(token, cb) { var key = (_.isString(token)) ? token : token.access_token; this.cache.get(key, function(err, reply) { if (err) { return cb(err); } if (!reply) { if (debug.enabled) { debug('cache miss: ' + key); } return cb(err, reply); } var token; try { token = JSON.parse(reply.toString()); } catch (err) { if (debug.enabled) { debug('err: ' + err); } cb(err, null); } // update expires_in var elapsed = new Date().getTime() - token.cached_at; token.expires_in = token.expires_in - (elapsed / 1000); delete(token.cached_at); if (debug.enabled) { debug('cache hit: ' + key); } cb(err, token); }); }; OAuthCache.prototype.deleteCachedToken = function(token) { var key = (_.isString(token)) ? token : token.access_token; this.cache.delete(key); }; /* * Generate an access token using client_credentials. Parameters: * clientId: required * clientSecret: required * scope: optional * tokenLifetime: lifetime in milliseconds, optional * attributes: hash of custom attributes to store and retrieve with token * * Returns an object with all the fields in the standard OAuth 2.0 response. */ OAuthCache.prototype.createTokenClientCredentials = function(options, cb) { var self = this; this.target.createTokenClientCredentials(options, function(err, reply) { self.cacheToken(err, reply, cb); }); }; /* * Generate an access token using password credentials. Parameters: * clientId: required * clientSecret: required * scope: optional * tokenLifetime: lifetime in milliseconds, optional * refreshTokenLifetime: lifetime in milliseconds, optional, defaults no expiration * username: required but not checked (must be checked outside this module) * password: required by not checked (must be checked outside this module) * attributes: hash of custom attributes to store and retrieve with token * * Returns an object with all the fields in the standard OAuth 2.0 response. */ OAuthCache.prototype.createTokenPasswordCredentials = function(options, cb) { var self = this; this.target.createTokenPasswordCredentials(options, function(err, reply) { self.cacheToken(err, reply, cb); }); }; /* * Generate an access token for authorization code once a code has been set up. Parameters: * clientId: required * clientSecret: required * code: Authorization code already generated by the "generateAuthorizationCode" method * redirectUri: The same redirect URI that was set in the call to generate the authorization code * tokenLifetime: lifetime in milliseconds, optional * refreshTokenLifetime: lifetime in milliseconds, optional, defaults no expiration * attributes: hash of custom attributes to store and retrieve with token * * Returns an object with all the fields in the standard OAuth 2.0 response. */ OAuthCache.prototype.createTokenAuthorizationCode = function(options, cb) { var self = this; this.target.createTokenAuthorizationCode(options, function(err, reply) { self.cacheToken(err, reply, cb); }); }; /* * Generate a redirect response for the authorization_code grant type. Parameters: * clientId: required * redirectUri: optional - if present, must match what was deployed along with the app * scope: optional * state: optional but certainly recommended * * Returns the redirect URI as a string. * 4.1.2 */ OAuthCache.prototype.generateAuthorizationCode = function(options, cb) { this.target.generateAuthorizationCode(options, cb); }; /* * Generate a redirect response for the implicit grant type. Parameters: * clientId: required * redirectUri: optional - if present, must match what was deployed along with the app * scope: optional * state: optional but certainly recommended * attributes: hash of custom attributes to store and retrieve with token * * Returns the redirect URI as a string. */ OAuthCache.prototype.createTokenImplicitGrant = function(options, cb) { var self = this; this.target.createTokenImplicitGrant(options, function(err, reply) { self.cacheToken(err, reply, cb); }); }; /* * Refresh an existing access token, and return a new token. Parameters: * clientId: required * clientSecret: required * refreshToken: required, from the original token grant * tokenLifetime: lifetime in milliseconds, optional * refreshTokenLifetime: lifetime in milliseconds, optional, defaults no expiration * scope: optional */ OAuthCache.prototype.refreshToken = function(options, cb) { var self = this; this.target.refreshToken(options, function(err, reply) { self.cacheToken(err, reply, cb); }); }; /* * Invalidate an existing token. Options is a hash containing: * clientId: required * clientSecret: required * token: required * tokenTypeHint: optional */ OAuthCache.prototype.invalidateToken = function(options, cb) { this.deleteCachedToken(options.token); this.target.invalidateToken(options, cb); }; /* * Validate an access token. */ OAuthCache.prototype.verifyToken = function(token, requiredScopes, cb) { var self = this; this.getCachedToken(token, function(err, reply) { if (reply) { if (requiredScopes) { // check scopes if (!Array.isArray(requiredScopes)) { requiredScopes = requiredScopes ? requiredScopes.split(' ') : []; } var grantedScopes = reply.scope || []; if (!Array.isArray(grantedScopes)) { grantedScopes = grantedScopes.split(' '); } if (_.difference(requiredScopes, grantedScopes).length > 0) { return cb(errorWithCode('invalid_scope')); } } cb(null, reply); } else { self.target.verifyToken(token, requiredScopes, function(err, reply) { if (err) { return cb(err); } reply.access_token = token; self.cacheToken(err, reply, cb); }); } }); }; OAuthCache.prototype.cacheApiKey = function(apiKey, cb) { try { var key = 'apiKey:' + apiKey; this.cache.set(key, apiKey); cb(null, apiKey); } catch (err) { debug('err: %s', err); cb(err); } }; OAuthCache.prototype.getCachedApiKey = function(apiKey, cb) { var key = 'apiKey:' + apiKey; this.cache.get(key, function(err, reply) { if (err) { return cb(err); } if (!reply) { debug('cache miss: %s', key); return cb(); } debug('cache hit: %s', key); cb(err, apiKey); }); }; /* * Validate an API Key. */ OAuthCache.prototype.verifyApiKey = function(apiKey, request, cb) { var self = this; this.getCachedApiKey(apiKey, function(err, reply) { if (err || reply) { return cb(err, !!reply); } self.target.verifyApiKey(apiKey, request, function(err) { if (err) { return cb(err); } self.cacheApiKey(apiKey, function(err, reply) { if (err) { return cb(err);} cb(null, !!reply); }); }); }); }; function errorWithCode(code) { var err = new Error(code); err.errorCode = code; return err; }