node-oauth2-server
Advanced tools
Comparing version 1.5.3-invalid-token to 2.0.0-beta3
@@ -5,8 +5,7 @@ ## Changelog | ||
### 1.5.2 | ||
- Fix expiration checking. (Previously the current time was mistakenly cached up instantiation) | ||
### 3.0 (in progress) | ||
- Huge refactor | ||
- Switch from internal router to exposing explit middleware to be added to individual routes | ||
- Switch all model save* functions to take two params, data and callback | ||
### 1.5.1 | ||
- Add repository to package.json | ||
### 1.5.0 | ||
@@ -60,2 +59,2 @@ - Add support for non-expiring tokens (set accessTokenLifetime/refreshTokenLifetime = null) | ||
- Use async crypto.randomBytes in token generation | ||
- Refactor structure, break into more files | ||
- Refactor structure, break into more files |
@@ -80,3 +80,3 @@ /** | ||
client.query('INSERT INTO oauth_refresh_tokens(refresh_token, client_id, user_id, ' + | ||
'expires) VALUES ($1, $2, $3, $4)', [accessToken, clientId, userId, expires], | ||
'expires) VALUES ($1, $2, $3, $4)', [refreshToken, clientId, userId, expires], | ||
function (err, result) { | ||
@@ -83,0 +83,0 @@ callback(err); |
@@ -17,118 +17,115 @@ /** | ||
// Modules | ||
var error = require('./error'); | ||
var error = require('./error'), | ||
runner = require('./runner'); | ||
var authorise = module.exports = {}; | ||
module.exports = Authorise; | ||
/** | ||
* Authorise a request with OAuth2 | ||
* This is the function order used by the runner | ||
* | ||
* This is a the top level function that should be directly | ||
* passed into the express callback chain to authorise a request | ||
* against OAuth2 | ||
* @type {Array} | ||
*/ | ||
var fns = [ | ||
getBearerToken, | ||
checkToken | ||
]; | ||
/** | ||
* Authorise | ||
* | ||
* @param {Object} req Connect request | ||
* @param {Object} res Connect response | ||
* @param {Function} next Connect next | ||
* @return {Void} | ||
* @param {Object} config Instance of OAuth object | ||
* @param {Object} req | ||
* @param {Object} res | ||
* @param {Function} next | ||
*/ | ||
authorise.handle = function (req, res, next) { | ||
// Get token | ||
var oauth = this; | ||
authorise.getBearerToken(req, function (err, bearerToken) { | ||
if (err) return next(err); | ||
function Authorise (config, req, next) { | ||
this.config = config; | ||
this.model = config.model; | ||
this.req = req; | ||
oauth.model.getAccessToken(bearerToken, function (err, token) { | ||
if (err) { | ||
return next(error('server_error', false, err)); | ||
} | ||
runner(fns, this, next); | ||
} | ||
authorise.validateAccessToken.call(oauth, token, req, next); | ||
}); | ||
}); | ||
}; | ||
/** | ||
* Validate Access Token | ||
* Get bearer token | ||
* | ||
* Check access token retrieved from storage is valid | ||
* Extract token from request according to RFC6750 | ||
* | ||
* @param {Object} token Connect token | ||
* @param {Object} req Connect req | ||
* @param {Function} next Connect next | ||
* @return {Void} | ||
* @param {Function} done | ||
* @this OAuth | ||
*/ | ||
authorise.validateAccessToken = function (token, req, next) { | ||
if (!token) { | ||
return next(error('invalid_token', 'The access token provided is invalid.')); | ||
} | ||
function getBearerToken (done) { | ||
var headerToken = this.req.get('Authorization'), | ||
getToken = this.req.query.access_token, | ||
postToken = this.req.body.access_token; | ||
// Check it's valid | ||
if (token.expires !== null && (!token.expires || token.expires < this.now)) { | ||
return next(error('invalid_token', 'The access token provided has expired.')); | ||
} | ||
// Check exactly one method was used | ||
var methodsUsed = (headerToken !== undefined) + (getToken !== undefined) + | ||
(postToken !== undefined); | ||
// Expose params | ||
req.oauth.token = token; | ||
req.user = token.user ? token.user : { id: token.user_id }; | ||
if (methodsUsed > 1) { | ||
return done(error('invalid_request', | ||
'Only one method may be used to authenticate at a time (Auth header, ' + | ||
'GET or POST).')); | ||
} else if (methodsUsed === 0) { | ||
return done(error('invalid_request', 'The access token was not found')); | ||
} | ||
next(); // Exit point | ||
}; | ||
// Header: http://tools.ietf.org/html/rfc6750#section-2.1 | ||
if (headerToken) { | ||
var matches = headerToken.match(/Bearer\s(\S+)/); | ||
/** | ||
* Extract access token from request | ||
* | ||
* Checks exactly one access token had been passed and | ||
* does additional validation for each method of passing | ||
* the token. | ||
* Returns OAuth2 Error if any of the above conditions | ||
* aren't met. | ||
* | ||
* @see OAuth2Server#authorizeRequest | ||
* | ||
* @param {Object} req Connect request | ||
* @return {Object|String} Oauth2Error or The access token | ||
*/ | ||
authorise.getBearerToken = function (req, next) { | ||
if (!matches) { | ||
return done(error('invalid_request', 'Malformed auth header')); | ||
} | ||
var headerToken = req.get('Authorization'), | ||
getToken = req.query.access_token, | ||
postToken = req.body.access_token; | ||
headerToken = matches[1]; | ||
} | ||
// Check exactly one method was used | ||
var methodsUsed = (headerToken !== undefined) + (getToken !== undefined) + | ||
(postToken !== undefined); | ||
// POST: http://tools.ietf.org/html/rfc6750#section-2.2 | ||
if (postToken) { | ||
if (this.req.method === 'GET') { | ||
return done(error('invalid_request', | ||
'Method cannot be GET When putting the token in the body.')); | ||
} | ||
if (methodsUsed > 1) { | ||
return next(error('invalid_request', | ||
'Only one method may be used to authenticate at a time (Auth header, GET or POST).')); | ||
} else if (methodsUsed === 0) { | ||
return next(error('invalid_request', 'The access token was not found')); | ||
} | ||
if (!this.req.is('application/x-www-form-urlencoded')) { | ||
return done(error('invalid_request', 'When putting the token in the ' + | ||
'body, content type must be application/x-www-form-urlencoded.')); | ||
} | ||
} | ||
// Header: http://tools.ietf.org/html/rfc6750#section-2.1 | ||
if (headerToken) { | ||
var matches = headerToken.match(/Bearer\s(\S+)/); | ||
this.bearerToken = headerToken || postToken || getToken; | ||
done(); | ||
} | ||
if (!matches) { | ||
return next(error('invalid_request', 'Malformed auth header')); | ||
} | ||
/** | ||
* Check token | ||
* | ||
* Check it against model, ensure it's not expired | ||
* @param {Function} done | ||
* @this OAuth | ||
*/ | ||
function checkToken (done) { | ||
var self = this; | ||
this.model.getAccessToken(this.bearerToken, function (err, token) { | ||
if (err) return done(error('server_error', false, err)); | ||
headerToken = matches[1]; | ||
} | ||
if (!token) { | ||
return done(error('invalid_token', | ||
'The access token provided is invalid.')); | ||
} | ||
// POST: http://tools.ietf.org/html/rfc6750#section-2.2 | ||
if (postToken) { | ||
if (req.method === 'GET') { | ||
return next(error('invalid_request', | ||
'Method cannot be GET When putting the token in the body.')); | ||
} | ||
if (token.expires !== null && | ||
(!token.expires || token.expires < new Date())) { | ||
return done(error('invalid_token', | ||
'The access token provided has expired.')); | ||
} | ||
if (!req.is('application/x-www-form-urlencoded')) { | ||
return next(error('invalid_request', 'When putting the token in the body, ' + | ||
'content type must be application/x-www-form-urlencoded.')); | ||
} | ||
} | ||
// Expose params | ||
self.req.oauth = { bearerToken: token }; | ||
self.req.user = token.user ? token.user : { id: token.user_id }; | ||
return next(null, headerToken || postToken || getToken); | ||
}; | ||
done(); | ||
}); | ||
} |
@@ -28,23 +28,23 @@ /** | ||
if (!(this instanceof OAuth2Error)) return new OAuth2Error(error, description, err); | ||
if (!(this instanceof OAuth2Error)) return new OAuth2Error(error, description, err); | ||
switch (error) { | ||
case 'invalid_client': | ||
case 'invalid_grant': | ||
case 'invalid_request': | ||
this.code = 400; | ||
break; | ||
switch (error) { | ||
case 'invalid_client': | ||
case 'invalid_grant': | ||
case 'invalid_request': | ||
this.code = 400; | ||
break; | ||
case 'invalid_token': | ||
this.code = 401; | ||
break; | ||
case 'server_error': | ||
this.code = 503; | ||
break; | ||
default: | ||
this.code = 500; | ||
} | ||
case 'server_error': | ||
this.code = 503; | ||
break; | ||
default: | ||
this.code = 500; | ||
} | ||
this.error = error; | ||
this.error_description = description || error; | ||
this.stack = (err && err.stack) || err; | ||
this.error = error; | ||
this.error_description = description || error; | ||
this.stack = (err && err.stack) || err; | ||
} |
@@ -17,8 +17,7 @@ /** | ||
// Required modules | ||
var error = require('./error'), | ||
authorise = require('./authorise'), | ||
token = require('./token'); | ||
AuthCodeGrant = require('./authCodeGrant'), | ||
Authorise = require('./authorise'), | ||
Grant = require('./grant'); | ||
// Expose | ||
module.exports = OAuth2Server; | ||
@@ -29,107 +28,159 @@ | ||
* | ||
* @param {Object|Void} config Configuration object | ||
* @param {Object} config Configuration object | ||
*/ | ||
function OAuth2Server (config) { | ||
if (!(this instanceof OAuth2Server)) { | ||
return new OAuth2Server(config); | ||
} | ||
if (!(this instanceof OAuth2Server)) return new OAuth2Server(config); | ||
config = config || {}; | ||
config = config || {}; | ||
if (!config.model) throw new Error('No model supplied to OAuth2Server'); | ||
this.model = config.model; | ||
if (!config.model) throw new Error('No model supplied to OAuth2Server'); | ||
this.model = config.model; | ||
this.allow = config.allow || []; | ||
this.grants = config.grants || []; | ||
this.debug = config.debug || false; | ||
this.passthroughErrors = config.passthroughErrors; | ||
this.grants = config.grants || []; | ||
this.debug = config.debug || false; | ||
this.passthroughErrors = config.passthroughErrors; | ||
this.accessTokenLifetime = config.accessTokenLifetime !== undefined ? | ||
config.accessTokenLifetime : 3600; | ||
this.refreshTokenLifetime = config.refreshTokenLifetime !== undefined ? | ||
config.refreshTokenLifetime : 1209600; | ||
this.authCodeLifetime = config.authCodeLifetime || 30; | ||
this.accessTokenLifetime = config.accessTokenLifetime !== undefined ? | ||
config.accessTokenLifetime : 3600; | ||
this.refreshTokenLifetime = config.refreshTokenLifetime !== undefined ? | ||
config.refreshTokenLifetime : 1209600; | ||
this.authCodeLifetime = config.authCodeLifetime || 30; | ||
this.regex = {}; | ||
this.regex.clientId = config.clientIdRegex || /^[a-z0-9-_]{3,40}$/i; | ||
this.regex.grantType = new RegExp('^(' + this.grants.join('|') + ')$', 'i'); | ||
this.regex = { | ||
clientId: config.clientIdRegex || /^[a-z0-9-_]{3,40}$/i, | ||
grantType: new RegExp('^(' + this.grants.join('|') + ')$', 'i') | ||
}; | ||
} | ||
/** | ||
* Authorise incoming requests | ||
* Authorisation Middleware | ||
* | ||
* Provides main OAuth middleware that passes oauth | ||
* authorization/token requests to relevant handlers or, | ||
* if it isn't allowed, passes it on to the internal | ||
* authorization handler | ||
* Returns middleware that will authorise the request using oauth, | ||
* if successful it will allow the request to proceed to the next handler | ||
* | ||
* @return {Function} Main OAuth handling middleware | ||
* @return {Function} middleware | ||
*/ | ||
OAuth2Server.prototype.handler = function () { | ||
var allowed = this.allow, | ||
allowedIsArray = Array.isArray(allowed), | ||
allowCache = allowedIsArray ? false : {}, | ||
oauth = this; | ||
OAuth2Server.prototype.authorise = function () { | ||
var self = this; | ||
return function (req, res, next) { | ||
var method = req.method.toLowerCase(), | ||
allow = allowedIsArray ? allowCache : allowCache[method]; | ||
return function (req, res, next) { | ||
new Authorise(self, req, next); | ||
}; | ||
}; | ||
// Build allow object this method if haven't yet already | ||
if (!allow) { | ||
var paths = allowedIsArray ? allowed : | ||
Array.prototype.concat(allowed.all || [], allowed[method] || []); | ||
/** | ||
* Grant Middleware | ||
* | ||
* Returns middleware that will grant tokens to valid requests. | ||
* This would normally be mounted at '/oauth/token' e.g. | ||
* | ||
* `app.all('/oauth/token', oauth.grant());` | ||
* | ||
* @return {Function} middleware | ||
*/ | ||
OAuth2Server.prototype.grant = function () { | ||
var self = this; | ||
allow = { | ||
len: paths.length, | ||
regex: new RegExp('^(' + paths.join('|') + ')$') | ||
}; | ||
return function (req, res, next) { | ||
new Grant(self, req, res, next); | ||
}; | ||
}; | ||
if (allowedIsArray) { | ||
allowCache = allow; | ||
} else { | ||
allowCache[method] = allow; | ||
} | ||
} | ||
/** | ||
* Code Auth Grant Middleware | ||
* | ||
* @param {Function} check Function will be called with req to check if the | ||
* user has authorised the request. | ||
* @return {Function} middleware | ||
*/ | ||
OAuth2Server.prototype.authCodeGrant = function (check) { | ||
var self = this; | ||
// Setup request params | ||
req.oauth = { internal: false }; | ||
oauth.now = new Date(); | ||
if (req.path === '/oauth/token') { | ||
req.oauth.internal = true; | ||
return token.handle.apply(oauth, arguments); | ||
} else if (!allow.len || !req.path.match(allow.regex)) { | ||
return authorise.handle.apply(oauth, arguments); | ||
} else { | ||
return next(); | ||
} | ||
}; | ||
return function (req, res, next) { | ||
new AuthCodeGrant(self, req, res, next, check); | ||
}; | ||
}; | ||
/** | ||
* Error Handler | ||
* OAuth Error Middleware | ||
* | ||
* Provides OAuth error handling middleware to catch any errors | ||
* and ensure an oauth complient response | ||
* Returns middleware that will catch OAuth errors and ensure an OAuth | ||
* complaint response | ||
* | ||
* @return {Function} OAuth error handling middleware | ||
* @return {Function} middleware | ||
*/ | ||
OAuth2Server.prototype.errorHandler = function () { | ||
var oauth = this; | ||
var self = this; | ||
return function (err, req, res, next) { | ||
if (err instanceof Error && err.status && err.status === 400) { | ||
err = error('invalid_request', err.toString(), err); | ||
} else if (!(err instanceof error)) { | ||
err = error('server_error', false, err); | ||
} | ||
return function (err, req, res, next) { | ||
if (!(err instanceof error) || self.passthroughErrors) return next(err); | ||
if (oauth.debug) console.log(err.stack || err); | ||
if (oauth.passthroughErrors && !req.oauth.internal) return next(err); | ||
if (self.debug) console.log(err.stack || err); | ||
delete err.stack; | ||
res.send(err.code, err); | ||
}; | ||
delete err.stack; | ||
res.send(err.code, err); | ||
}; | ||
}; | ||
/** | ||
* Lockdown | ||
* | ||
* When using the lockdown patter, this function should be called after | ||
* all routes have been declared. | ||
* It will search through each route and if it has not been explitly bypassed | ||
* (by passing oauth.bypass) then authorise will be inserted. | ||
* If oauth.grant has been passed it will replace it with the proper grant | ||
* middleware | ||
* NOTE: When using this method, you must PASS the method not CALL the method, | ||
* e.g.: | ||
* | ||
* ` | ||
* app.all('/oauth/token', app.oauth.grant); | ||
* | ||
* app.get('/secrets', function (req, res) { | ||
* res.send('secrets'); | ||
* }); | ||
* | ||
* app.get('/public', app.oauth.bypass, function (req, res) { | ||
* res.send('publci'); | ||
* }); | ||
* | ||
* app.oauth.lockdown(app); | ||
* ` | ||
* | ||
* @param {Object} app Express app | ||
*/ | ||
OAuth2Server.prototype.lockdown = function (app) { | ||
var self = this; | ||
var lockdown = function (route) { | ||
// Check if it's a grant route | ||
var pos = route.callbacks.indexOf(self.grant); | ||
if (pos !== -1) { | ||
route.callbacks[pos] = self.grant(); | ||
return; | ||
} | ||
// Check it's not been explitly bypassed | ||
pos = route.callbacks.indexOf(self.bypass); | ||
if (pos === -1) { | ||
route.callbacks.unshift(self.authorise()); | ||
} else { | ||
route.callbacks.splice(pos, 1); | ||
} | ||
}; | ||
for (var method in app.routes) { | ||
app.routes[method].forEach(lockdown); | ||
} | ||
}; | ||
/** | ||
* Bypass | ||
* | ||
* This is used as placeholder for when using the lockdown pattern | ||
* | ||
* @return {Function} noop | ||
*/ | ||
OAuth2Server.prototype.bypass = function () {}; |
334
lib/token.js
@@ -16,319 +16,43 @@ /** | ||
*/ | ||
// Modules | ||
var crypto = require('crypto'), | ||
error = require('./error'); | ||
error = require('./error'); | ||
var token = module.exports = {}; | ||
module.exports = Token; | ||
/** | ||
* Token endpoint | ||
* Token generator that will delegate to model or | ||
* the internal random generator | ||
* | ||
* @param {Object} req Connect request | ||
* @param {Object} res Connect response | ||
* @param {Function} next Connect next | ||
* @return {Void} | ||
* @param {String} type 'accessToken' or 'refreshToken' | ||
* @param {Function} callback | ||
*/ | ||
token.handle = function (req, res, next) { | ||
// Only POST via application/x-www-form-urlencoded is acceptable | ||
if (req.method !== 'POST' || !req.is('application/x-www-form-urlencoded')) { | ||
return next(error('invalid_request', | ||
'Method must be POST with application/x-www-form-urlencoded encoding')); | ||
} | ||
function Token (config, type, callback) { | ||
if (config.model.generateToken) { | ||
config.model.generateToken(type, config.req, function (err, token) { | ||
if (err) return callback(error('server_error', false, err)); | ||
if (!token) return generateRandomToken(callback); | ||
callback(false, token); | ||
}); | ||
} else { | ||
generateRandomToken(callback); | ||
} | ||
} | ||
// Grant type | ||
req.oauth.grantType = req.body && req.body.grant_type; | ||
if (!req.oauth.grantType || !req.oauth.grantType.match(this.regex.grantType)) { | ||
return next(error('invalid_request', 'Invalid or missing grant_type parameter')); | ||
} | ||
// Extract credentials | ||
// http://tools.ietf.org/html/rfc6749#section-3.2.1 | ||
var creds = token.getClientCredentials(req); | ||
if (!creds.client_id || !creds.client_id.match(this.regex.clientId)) { | ||
return next(error('invalid_client', 'Invalid or missing client_id parameter')); | ||
} else if (!creds.client_secret) { | ||
return next(error('invalid_client', 'Missing client_secret parameter')); | ||
} | ||
// Check credentials against model | ||
var oauth = this; | ||
this.model.getClient(creds.client_id, creds.client_secret, function (err, client) { | ||
if (err) { | ||
return next(error('server_error', false, err)); | ||
} | ||
if (!client) { | ||
return next(error('invalid_client', 'The client credentials are invalid')); | ||
} | ||
req.oauth.client = client; | ||
oauth.model.grantTypeAllowed(client.client_id, req.oauth.grantType, function (err, allowed) { | ||
if (!allowed) { | ||
return next(error('invalid_client', | ||
'The grant type is unauthorised for this client_id')); | ||
} | ||
token.grant.call(oauth, req, res, next); | ||
}); | ||
}); | ||
}; | ||
/** | ||
* Convinience function for extracting client credentials from request | ||
* Internal random token generator | ||
* | ||
* @see OAuth2Server#token | ||
* | ||
* @param {Object} req Connect request | ||
* @return {Object} Client id/secret from headers or body | ||
* @param {Function} callback | ||
*/ | ||
token.getClientCredentials = function (req) { | ||
var generateRandomToken = function (callback) { | ||
crypto.randomBytes(256, function (ex, buffer) { | ||
if (ex) return callback(error('server_error')); | ||
// Return object | ||
var creds = function (clientId, clientSecret) { | ||
this.client_id = clientId; | ||
this.client_secret = clientSecret; | ||
}; | ||
var token = crypto | ||
.createHash('sha1') | ||
.update(buffer) | ||
.digest('hex'); | ||
// Check for Basic Auth | ||
// Pulled from Connect: | ||
// https://github.com/senchalabs/connect/blob/master/lib/middleware/basicAuth.js#L65 | ||
var fromBasicAuth = function () { | ||
var authorization = req.get('authorization'); | ||
if (!authorization) return false; | ||
var parts = authorization.split(' '); | ||
if (parts.length !== 2) return false; | ||
var scheme = parts[0], | ||
credentials = new Buffer(parts[1], 'base64').toString().replace(/^\s+|\s+$/g, ""), | ||
index = credentials.indexOf(':'); | ||
if (scheme !== 'Basic' || index < 0) return false; | ||
return new creds(credentials.slice(0, index), credentials.slice(index + 1)); | ||
}; | ||
return fromBasicAuth() || new creds(req.body.client_id, req.body.client_secret); | ||
callback(false, token); | ||
}); | ||
}; | ||
/** | ||
* Grant access token based on grant_type | ||
* | ||
* @see OAuth2Server#token | ||
* | ||
* @param {Object} req Connect request | ||
* @param {Object} res Connect response | ||
* @param {Function} next Connect next | ||
* @return {Void} | ||
*/ | ||
token.grant = function (req, res, next) { | ||
var invalid = function () { | ||
next(error('invalid_request', 'Invalid grant_type parameter or parameter missing')); | ||
}; | ||
var oauth = this; | ||
if (req.oauth.grantType.match(/^http(s|):\/\//) && this.model.extendedGrant) { | ||
return this.model.extendedGrant(req, function (err, supported, user) { | ||
if (err && err.error && err.description) { | ||
return next(error(err.error, err.description)); | ||
} | ||
if (err) return next(err); | ||
if (!supported) return invalid(); | ||
if (!user || user.id === undefined) { | ||
return next(error('invalid_request', 'Invalid request.')); | ||
} | ||
req.user = user; | ||
token.grantAccessToken.call(oauth, req, res, next); | ||
}); | ||
} | ||
switch (req.oauth.grantType) { | ||
case 'password': | ||
// User credentials | ||
var uname = req.body.username, | ||
pword = req.body.password; | ||
if (!uname || !pword) { | ||
return next(error('invalid_client', | ||
'Missing parameters. "username" and "password" are required')); | ||
} | ||
return this.model.getUser(uname, pword, function (err, user) { | ||
if (err) { | ||
return next(error('server_error', false, err)); | ||
} | ||
if (user) { | ||
req.user = user; | ||
token.grantAccessToken.call(oauth, req, res, next); | ||
} else { | ||
next(error('invalid_grant', 'User credentials are invalid')); | ||
} | ||
}); | ||
case 'refresh_token': | ||
if (!req.body.refresh_token) { | ||
return next(error('invalid_request', 'No "refresh_token" parameter')); | ||
} | ||
return this.model.getRefreshToken(req.body.refresh_token, function (err, refreshToken) { | ||
if (err) return next(error('server_error', false, err)); | ||
if (!refreshToken || refreshToken.client_id !== req.oauth.client.client_id) { | ||
return next(error('invalid_grant', 'Invalid refresh token')); | ||
} else if (refreshToken.expires !== null && refreshToken.expires < oauth.now) { | ||
return next(error('invalid_grant', 'Refresh token has expired')); | ||
} | ||
if (refreshToken.user_id) { | ||
req.user = { id: refreshToken.user_id }; | ||
} else { | ||
return next(error('server_error', false, | ||
'No user/user_id parameter returned from getRefreshToken')); | ||
} | ||
if (oauth.model.revokeRefreshToken) { | ||
oauth.model.revokeRefreshToken(req.body.refresh_token, function (err) { | ||
if (err) return next(error('server_error', false, err)); | ||
token.grantAccessToken.call(oauth, req, res, next); | ||
}); | ||
} else { | ||
return token.grantAccessToken.call(oauth, req, res, next); | ||
} | ||
}); | ||
default: | ||
return invalid(); | ||
} | ||
}; | ||
/** | ||
* Save access token ready for issuing | ||
* | ||
* @see OAuth2Server#grant | ||
* | ||
* @param {Object} req Connect request | ||
* @param {Object} res Connect response | ||
* @param {Function} next Connect next | ||
* @return {Void} | ||
*/ | ||
token.grantAccessToken = function (req, res, next) { | ||
var oauth = this; | ||
var createRefreshToken = function (err, refreshToken) { | ||
if (err || !refreshToken) return next(err); | ||
// Object indicates a reissue | ||
if (typeof refreshToken === 'object' && refreshToken.refresh_token) { | ||
req.oauth.accessToken.refresh_token = { refresh_token: refreshToken.refresh_token }; | ||
return token.issueToken.call(oauth, req, res, next); | ||
} | ||
req.oauth.accessToken.refresh_token = refreshToken; | ||
var expires = null; | ||
if (oauth.refreshTokenLifetime !== null) { | ||
expires = new Date(oauth.now); | ||
expires.setSeconds(expires.getSeconds() + oauth.refreshTokenLifetime); | ||
} | ||
oauth.model.saveRefreshToken(req.oauth.accessToken.refresh_token, | ||
req.oauth.client.client_id, req.user.id, expires, function (err) { | ||
if (err) return next(error('server_error', false, err)); | ||
token.issueToken.call(oauth, req, res, next); | ||
}); | ||
}; | ||
var issueRefreshToken = function () { | ||
// Are we issuing refresh tokens? | ||
if (oauth.grants.indexOf('refresh_token') >= 0) { | ||
token.generateToken.call(oauth, 'refreshToken', req, createRefreshToken); | ||
} else { | ||
token.issueToken.call(oauth, req, res, next); | ||
} | ||
}; | ||
var createAccessToken = function (err, accessToken) { | ||
if (err || !accessToken) return next(err); | ||
// Object idicates a reissue | ||
if (typeof accessToken === 'object' && accessToken.access_token) { | ||
req.oauth.accessToken = { access_token: accessToken.access_token }; | ||
return issueRefreshToken(); | ||
} | ||
req.oauth.accessToken = { access_token: accessToken }; | ||
var expires = null; | ||
if (oauth.accessTokenLifetime !== null) { | ||
expires = new Date(oauth.now); | ||
expires.setSeconds(expires.getSeconds() + oauth.accessTokenLifetime); | ||
} | ||
oauth.model.saveAccessToken(req.oauth.accessToken.access_token, req.oauth.client.client_id, | ||
req.user.id, expires, function (err) { | ||
if (err) return next(error('server_error', false, err)); | ||
issueRefreshToken(); | ||
}); | ||
}; | ||
token.generateToken.call(oauth, 'accessToken', req, createAccessToken); | ||
}; | ||
/** | ||
* Actually issue the token and send the response | ||
* | ||
* @see OAuth2Server#grantAccessToken | ||
* | ||
* @param {Object} req Connect request | ||
* @param {Object} res Connect response | ||
* @param {Function} next Connect next | ||
* @return {Void} | ||
*/ | ||
token.issueToken = function (req, res, next) { | ||
// Prepare for output | ||
req.oauth.accessToken.token_type = 'bearer'; | ||
if (this.accessTokenLifetime !== null) { | ||
req.oauth.accessToken.expires_in = this.accessTokenLifetime; | ||
} | ||
// That's it! | ||
res.set({ 'Cache-Control': 'no-cache', 'Pragma': 'no-cache' }); | ||
res.jsonp(req.oauth.accessToken); | ||
}; | ||
/** | ||
* Convinience function for generating a token | ||
* | ||
* @param {String} req Connect req | ||
* @param {Function} next Connect next | ||
* @param {Function} callback | ||
* @return {Void} | ||
*/ | ||
token.generateToken = function (type, req, callback) { | ||
if (this.model.generateToken) { | ||
this.model.generateToken(type, req, function (err, generatedToken) { | ||
if (err) return callback(error('server_error', false, err)); | ||
if (!generatedToken) return token._generateToken(callback); | ||
callback(false, generatedToken); | ||
}); | ||
} else { | ||
token._generateToken(callback); | ||
} | ||
}; | ||
token._generateToken= function (callback) { | ||
crypto.randomBytes(256, function (ex, buffer) { | ||
if (ex) return callback(error('server_error')); | ||
callback(false, crypto.createHash('sha1').update(buffer).digest('hex')); | ||
}); | ||
}; |
{ | ||
"name": "node-oauth2-server", | ||
"description": "Complete, compliant and well tested module for implementing an OAuth2 Server/Provider with express in node.js", | ||
"version": "1.5.3-invalid-token", | ||
"keywords": [ | ||
"oauth", | ||
"oauth2" | ||
], | ||
"author": { | ||
"name": "NightWorld", | ||
"email": "code@nightworld.com" | ||
}, | ||
"contributors": [ | ||
{ | ||
"name": "Thom Seddon", | ||
"email": "thom@nightworld.com" | ||
} | ||
], | ||
"main": "lib/oauth2server.js", | ||
"dependencies": {}, | ||
"devDependencies": { | ||
"express": "3.1.x", | ||
"mocha": "1.8.x", | ||
"should": "1.2.x", | ||
"supertest": "0.5.x" | ||
}, | ||
"licenses": [ | ||
{ | ||
"type": "Apache 2.0", | ||
"url": "http://www.apache.org/licenses/LICENSE-2.0.html" | ||
} | ||
], | ||
"engines": { | ||
"node": ">=0.8" | ||
}, | ||
"scripts": { | ||
"test": "mocha" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/nightworld/node-oauth2-server.git" | ||
} | ||
"name": "node-oauth2-server", | ||
"description": "Complete, compliant and well tested module for implementing an OAuth2 Server/Provider with express in node.js", | ||
"version": "2.0.0-beta3", | ||
"keywords": [ | ||
"oauth", | ||
"oauth2" | ||
], | ||
"author": { | ||
"name": "NightWorld", | ||
"email": "code@nightworld.com" | ||
}, | ||
"contributors": [ | ||
{ | ||
"name": "Thom Seddon", | ||
"email": "thom@nightworld.com" | ||
} | ||
], | ||
"main": "lib/oauth2server.js", | ||
"dependencies": {}, | ||
"devDependencies": { | ||
"express": "3.1.x", | ||
"mocha": "1.8.x", | ||
"should": "1.2.x", | ||
"supertest": "0.5.x" | ||
}, | ||
"licenses": [ | ||
{ | ||
"type": "Apache 2.0", | ||
"url": "http://www.apache.org/licenses/LICENSE-2.0.html" | ||
} | ||
], | ||
"engines": { | ||
"node": ">=0.8" | ||
}, | ||
"scripts": { | ||
"test": "mocha" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/nightworld/node-oauth2-server.git" | ||
} | ||
} |
103
Readme.md
@@ -5,8 +5,5 @@ # Node OAuth2 Server [![Build Status](https://travis-ci.org/nightworld/node-oauth2-server.png?branch=master)](https://travis-ci.org/nightworld/node-oauth2-server) | ||
## 2.0 | ||
Version is under active development, for a preview see the 2.0 branch: https://github.com/nightworld/node-oauth2-server/tree/2.0 | ||
## Installation | ||
$ npm install node-oauth2-server | ||
$ npm install node-oauth2-server | ||
@@ -19,3 +16,3 @@ ## Quick Start | ||
var express = require('express'), | ||
oauthserver = require('node-oauth2-server'); | ||
oauthserver = require('node-oauth2-server'); | ||
@@ -25,16 +22,18 @@ var app = express(); | ||
app.configure(function() { | ||
var oauth = oauthserver({ | ||
model: {}, // See below for specification | ||
grants: ['password'], | ||
debug: true | ||
}); | ||
app.use(express.bodyParser()); // REQUIRED | ||
app.use(oauth.handler()); | ||
app.use(oauth.errorHandler()); | ||
app.oauth = oauthserver({ | ||
model: {}, // See below for specification | ||
grants: ['password'], | ||
debug: true | ||
}); | ||
app.use(express.bodyParser()); // REQUIRED | ||
}); | ||
app.get('/', function (req, res) { | ||
res.send('Secret area'); | ||
app.all('/oauth/token', app.oauth.grant()); | ||
app.get('/', app.oauth.authorise(), function (req, res) { | ||
res.send('Secret area'); | ||
}); | ||
app.use(app.oauth.errorHandler()); | ||
app.listen(3000); | ||
@@ -49,10 +48,6 @@ ``` | ||
- Supports password, refresh_token and extension (custom) grant types | ||
- Supports authorization_code, password, refresh_token and extension (custom) grant types | ||
- Implicitly supports any form of storage e.g. PostgreSQL, MySQL, Mongo, Redis... | ||
- Full test suite | ||
## Limitations | ||
- Does not yet support authorization code grant type | ||
## Options | ||
@@ -62,7 +57,2 @@ | ||
- Model object (see below) | ||
- *array|object* **allow** | ||
- Paths to allow to bypass authorisation, can take either form: | ||
- array, all methods allowed: `['/path1', '/path2']` | ||
- object or arrays keyed by method: `{ get: ['/path1'], post: ['/path2'], all: ['/path3'] }` | ||
- Default: `[]` | ||
- *array* **grants** | ||
@@ -74,4 +64,2 @@ - grant types you wish to support, currently the module supports `password` and `refresh_token` | ||
- Default: `false` | ||
- *boolean* **passthroughErrors** | ||
- If true, **non grant** errors will not be handled internally (so you can ensure a consistent format with the rest of your api) | ||
- Default: `false` | ||
@@ -92,2 +80,4 @@ - *number* **accessTokenLifetime** | ||
- Default: `/^[a-z0-9-_]{3,40}$/i` | ||
- *boolean* **passthroughErrors** | ||
- If true, **non grant** errors will not be handled internally (so you can ensure a consistent format with the rest of your api) | ||
@@ -139,7 +129,8 @@ ## Model Specification | ||
#### saveAccessToken (accessToken, clientId, userId, expires, callback) | ||
- *string* **accessToken** | ||
- *string* **clientId** | ||
- *string|number* **userId** | ||
- *date* **expires** | ||
#### saveAccessToken (accessToken, callback) | ||
- *object* **accessToken** | ||
- *string* **accessToken** | ||
- *string* **clientId** | ||
- *string|number* **userId** | ||
- *date* **expires** | ||
- *function* **callback (error)** | ||
@@ -150,2 +141,31 @@ - *mixed* **error** | ||
### Required for `authorization_code` grant type | ||
#### getAuthCode (authCode, callback) | ||
- *string* **authCode** | ||
- *function* **callback (error, authCode)** | ||
- *mixed* **error** | ||
- Truthy to indicate an error | ||
- *object* **authCode** | ||
- The authorization code retrieved form storage or falsey to indicate invalid code | ||
- Must contain the following keys: | ||
- *string|number* **client_id** | ||
- client_id associated with this auth code | ||
- *date* **expires** | ||
- The date when it expires | ||
- *string|number* **user_id** | ||
- The user_id | ||
#### saveAuthCode (authCode, callback) | ||
- *object* **authCode** | ||
- *string* **auth_code** | ||
- *string* **client_id** | ||
- *date* **expires** | ||
- *mixed* **user** | ||
- Whatever was passed as `user` to the codeGrant function (see example) | ||
- *function* **callback (error)** | ||
- *mixed* **error** | ||
- Truthy to indicate an error | ||
### Required for `password` grant type | ||
@@ -167,7 +187,8 @@ | ||
#### saveRefreshToken (refreshToken, clientId, userId, expires, callback) | ||
- *string* **refreshToken** | ||
- *string* **clientId** | ||
- *string|number* **userId** | ||
- *date* **expires** | ||
#### saveRefreshToken (refreshToken, callback) | ||
- *object* **refreshToken** | ||
- *string* **refreshToken** | ||
- *string* **clientId** | ||
- *string|number* **userId** | ||
- *date* **expires** | ||
- *function* **callback (error)** | ||
@@ -272,6 +293,6 @@ - *mixed* **error** | ||
{ | ||
"access_token":"2YotnFZFEjr1zCsicMWpAA", | ||
"token_type":"bearer", | ||
"expires_in":3600, | ||
"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA" | ||
"access_token":"2YotnFZFEjr1zCsicMWpAA", | ||
"token_type":"bearer", | ||
"expires_in":3600, | ||
"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA" | ||
} | ||
@@ -278,0 +299,0 @@ ``` |
@@ -17,6 +17,5 @@ /** | ||
var assert = require('assert'), | ||
express = require('express'), | ||
request = require('supertest'), | ||
should = require('should'); | ||
var express = require('express'), | ||
request = require('supertest'), | ||
should = require('should'); | ||
@@ -26,62 +25,66 @@ var oauth2server = require('../'); | ||
var bootstrap = function (oauthConfig) { | ||
var app = express(), | ||
oauth = oauth2server(oauthConfig || { model: {} }); | ||
var app = express(), | ||
oauth = oauth2server(oauthConfig || { model: {} }); | ||
app.use(express.bodyParser()); | ||
app.use(oauth.handler()); | ||
app.use(oauth.errorHandler()); | ||
app.use(express.bodyParser()); | ||
if (oauthConfig && oauthConfig.passthroughErrors) { | ||
app.use(function (err, req, res, next) { | ||
res.send('passthrough'); | ||
}); | ||
} | ||
app.all('/oauth/token', oauth.grant()); | ||
app.all('/', oauth.authorise(), function (req, res) { | ||
res.send('Hello World'); | ||
}); | ||
return app; | ||
app.use(oauth.errorHandler()); | ||
if (oauthConfig && oauthConfig.passthroughErrors) { | ||
app.use(function (err, req, res, next) { | ||
res.send('passthrough'); | ||
}); | ||
} | ||
return app; | ||
}; | ||
describe('OAuth2Server.errorHandler()', function() { | ||
it('should return an oauth conformat response', function (done) { | ||
var app = bootstrap(); | ||
describe('Error Handler', function() { | ||
it('should return an oauth conformat response', function (done) { | ||
var app = bootstrap(); | ||
request(app) | ||
.get('/') | ||
.expect(400) | ||
.end(function (err, res) { | ||
if (err) return done(err); | ||
request(app) | ||
.get('/') | ||
.expect(400) | ||
.end(function (err, res) { | ||
if (err) return done(err); | ||
res.body.should.have.keys('code', 'error', 'error_description'); | ||
res.body.should.have.keys('code', 'error', 'error_description'); | ||
res.body.code.should.be.a('number'); | ||
res.body.code.should.equal(res.statusCode); | ||
res.body.code.should.be.a('number'); | ||
res.body.code.should.equal(res.statusCode); | ||
res.body.error.should.be.a('string'); | ||
res.body.error.should.be.a('string'); | ||
res.body.error_description.should.be.a('string'); | ||
res.body.error_description.should.be.a('string'); | ||
done(); | ||
}); | ||
}); | ||
done(); | ||
}); | ||
}); | ||
it('should passthrough non grant errors if requested', function (done) { | ||
var app = bootstrap({ | ||
passthroughErrors: true, | ||
model: {} | ||
}); | ||
it('should passthrough authorise errors', function (done) { | ||
var app = bootstrap({ | ||
passthroughErrors: true, | ||
model: {} | ||
}); | ||
request(app) | ||
.get('/') | ||
.expect(200, /^passthrough$/, done); | ||
}); | ||
request(app) | ||
.get('/') | ||
.expect(200, /^passthrough$/, done); | ||
}); | ||
it('should never passthrough grant errors', function (done) { | ||
var app = bootstrap({ | ||
passthroughErrors: true, | ||
model: {} | ||
}); | ||
it('should passthrough grant errors', function (done) { | ||
var app = bootstrap({ | ||
passthroughErrors: true, | ||
model: {} | ||
}); | ||
request(app) | ||
.post('/oauth/token') | ||
.expect(400, done); | ||
}); | ||
request(app) | ||
.post('/oauth/token') | ||
.expect(200, /^passthrough$/, done); | ||
}); | ||
}); |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
113271
28
2690
303
1