236 lines
7.6 KiB
JavaScript
236 lines
7.6 KiB
JavaScript
/**
|
|
* Module dependencies.
|
|
*/
|
|
var url = require('url')
|
|
, utils = require('../utils')
|
|
, AuthorizationError = require('../errors/authorizationerror');
|
|
|
|
|
|
/**
|
|
* Handles requests to obtain a grant in the form of an authorization code.
|
|
*
|
|
* Callbacks:
|
|
*
|
|
* This middleware requires an `issue` callback, for which the function
|
|
* signature is as follows:
|
|
*
|
|
* function(client, redirectURI, user, ares, done) { ... }
|
|
*
|
|
* `client` is the client instance making the authorization request.
|
|
* `redirectURI` is the redirect URI specified by the client, and used as a
|
|
* verifier in the subsequent access token exchange. `user` is the
|
|
* authenticated user approving the request. `ares` is any additional
|
|
* parameters parsed from the user's decision, including scope, duration of
|
|
* access, etc. `done` is called to issue an authorization code:
|
|
*
|
|
* done(err, code)
|
|
*
|
|
* `code` is the code that will be sent to the client. If an error occurs,
|
|
* `done` should be invoked with `err` set in idomatic Node.js fashion.
|
|
*
|
|
* The code issued in this step will be used by the client in exchange for an
|
|
* access token. This code is bound to the client identifier and redirection
|
|
* URI, which is included in the token request for verification. The code is a
|
|
* single-use token, and should expire shortly after it is issued (the maximum
|
|
* recommended lifetime is 10 minutes).
|
|
*
|
|
* Options:
|
|
*
|
|
* scopeSeparator separator used to demarcate scope values (default: ' ')
|
|
*
|
|
* Examples:
|
|
*
|
|
* server.grant(oauth2orize.grant.code(function(client, redirectURI, user, ares, done) {
|
|
* AuthorizationCode.create(client.id, redirectURI, user.id, ares.scope, function(err, code) {
|
|
* if (err) { return done(err); }
|
|
* done(null, code);
|
|
* });
|
|
* }));
|
|
*
|
|
* References:
|
|
* - [Authorization Code](http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-1.3.1)
|
|
* - [Authorization Code Grant](http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-4.1)
|
|
*
|
|
* @param {Object} options
|
|
* @param {Function} issue
|
|
* @return {Object} module
|
|
* @api public
|
|
*/
|
|
module.exports = function code(options, issue, extend) {
|
|
if (typeof options == 'function') {
|
|
extend = issue;
|
|
issue = options;
|
|
options = undefined;
|
|
}
|
|
options = options || {};
|
|
extend = extend || function(txn, cb){ cb(); };
|
|
|
|
if (!issue) { throw new TypeError('oauth2orize.code grant requires an issue callback'); }
|
|
|
|
var modes = options.modes || {};
|
|
if (!modes.query) {
|
|
modes.query = require('../response/query');
|
|
}
|
|
|
|
// For maximum flexibility, multiple scope spearators can optionally be
|
|
// allowed. This allows the server to accept clients that separate scope
|
|
// with either space or comma (' ', ','). This violates the specification,
|
|
// but achieves compatibility with existing client libraries that are already
|
|
// deployed.
|
|
var separators = options.scopeSeparator || ' ';
|
|
if (!Array.isArray(separators)) {
|
|
separators = [ separators ];
|
|
}
|
|
|
|
|
|
/* Parse requests that request `code` as `response_type`.
|
|
*
|
|
* @param {http.ServerRequest} req
|
|
* @api public
|
|
*/
|
|
function request(req) {
|
|
var clientID = req.query.client_id
|
|
, redirectURI = req.query.redirect_uri
|
|
, scope = req.query.scope
|
|
, state = req.query.state;
|
|
|
|
if (!clientID) { throw new AuthorizationError('Missing required parameter: client_id', 'invalid_request'); }
|
|
if (typeof clientID !== 'string') { throw new AuthorizationError('Invalid parameter: client_id must be a string', 'invalid_request'); }
|
|
|
|
if (scope) {
|
|
if (typeof scope !== 'string') {
|
|
throw new AuthorizationError('Invalid parameter: scope must be a string', 'invalid_request');
|
|
}
|
|
|
|
for (var i = 0, len = separators.length; i < len; i++) {
|
|
var separated = scope.split(separators[i]);
|
|
// only separate on the first matching separator. this allows for a sort
|
|
// of separator "priority" (ie, favor spaces then fallback to commas)
|
|
if (separated.length > 1) {
|
|
scope = separated;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!Array.isArray(scope)) { scope = [ scope ]; }
|
|
}
|
|
|
|
return {
|
|
clientID: clientID,
|
|
redirectURI: redirectURI,
|
|
scope: scope,
|
|
state: state
|
|
};
|
|
}
|
|
|
|
/* Sends responses to transactions that request `code` as `response_type`.
|
|
*
|
|
* @param {Object} txn
|
|
* @param {http.ServerResponse} res
|
|
* @param {Function} next
|
|
* @api public
|
|
*/
|
|
function response(txn, res, complete, next) {
|
|
var mode = 'query'
|
|
, respond;
|
|
if (txn.req && txn.req.responseMode) {
|
|
mode = txn.req.responseMode;
|
|
}
|
|
respond = modes[mode];
|
|
|
|
if (!respond) {
|
|
// http://lists.openid.net/pipermail/openid-specs-ab/Week-of-Mon-20140317/004680.html
|
|
return next(new AuthorizationError('Unsupported response mode: ' + mode, 'unsupported_response_mode', null, 501));
|
|
}
|
|
if (respond && respond.validate) {
|
|
try {
|
|
respond.validate(txn);
|
|
} catch(ex) {
|
|
return next(ex);
|
|
}
|
|
}
|
|
|
|
if (!txn.res.allow) {
|
|
var params = { error: 'access_denied' };
|
|
if (txn.req && txn.req.state) { params.state = txn.req.state; }
|
|
return respond(txn, res, params);
|
|
}
|
|
|
|
function issued(err, code) {
|
|
if (err) { return next(err); }
|
|
if (!code) { return next(new AuthorizationError('Request denied by authorization server', 'access_denied')); }
|
|
|
|
var params = { code: code };
|
|
if (txn.req && txn.req.state) { params.state = txn.req.state; }
|
|
extend(txn, function(err, exparams) {
|
|
if (err) { return next(err); }
|
|
if (exparams) { utils.merge(params, exparams); }
|
|
complete(function(err) {
|
|
if (err) { return next(err); }
|
|
return respond(txn, res, params);
|
|
});
|
|
});
|
|
}
|
|
|
|
// NOTE: The `redirect_uri`, if present in the client's authorization
|
|
// request, must also be present in the subsequent request to exchange
|
|
// the authorization code for an access token. Acting as a verifier,
|
|
// the two values must be equal and serve to protect against certain
|
|
// types of attacks. More information can be found here:
|
|
//
|
|
// http://hueniverse.com/2011/06/oauth-2-0-redirection-uri-validation/
|
|
|
|
try {
|
|
var arity = issue.length;
|
|
if (arity == 7) {
|
|
issue(txn.client, txn.req.redirectURI, txn.user, txn.res, txn.req, txn.locals, issued);
|
|
} else if (arity == 6) {
|
|
issue(txn.client, txn.req.redirectURI, txn.user, txn.res, txn.req, issued);
|
|
} else if (arity == 5) {
|
|
issue(txn.client, txn.req.redirectURI, txn.user, txn.res, issued);
|
|
} else { // arity == 4
|
|
issue(txn.client, txn.req.redirectURI, txn.user, issued);
|
|
}
|
|
} catch (ex) {
|
|
return next(ex);
|
|
}
|
|
}
|
|
|
|
function errorHandler(err, txn, res, next) {
|
|
var mode = 'query'
|
|
, params = {}
|
|
, respond;
|
|
if (txn.req && txn.req.responseMode) {
|
|
mode = txn.req.responseMode;
|
|
}
|
|
respond = modes[mode];
|
|
|
|
if (!respond) {
|
|
return next(err);
|
|
}
|
|
if (respond && respond.validate) {
|
|
try {
|
|
respond.validate(txn);
|
|
} catch(ex) {
|
|
return next(err);
|
|
}
|
|
}
|
|
|
|
params.error = err.code || 'server_error';
|
|
if (err.message) { params.error_description = err.message; }
|
|
if (err.uri) { params.error_uri = err.uri; }
|
|
if (txn.req && txn.req.state) { params.state = txn.req.state; }
|
|
return respond(txn, res, params);
|
|
}
|
|
|
|
|
|
/**
|
|
* Return `code` approval module.
|
|
*/
|
|
var mod = {};
|
|
mod.name = 'code';
|
|
mod.request = request;
|
|
mod.response = response;
|
|
mod.error = errorHandler;
|
|
return mod;
|
|
};
|