243 lines
10 KiB
JavaScript
243 lines
10 KiB
JavaScript
/**
|
|
* Module dependencies.
|
|
*/
|
|
var utils = require('../utils')
|
|
, AuthorizationError = require('../errors/authorizationerror');
|
|
|
|
|
|
/**
|
|
* Handle authorization requests from OAuth 2.0 clients.
|
|
*
|
|
* Obtaining authorization via OAuth 2.0 consists of a sequence of discrete
|
|
* steps. First, the client requests authorization from the user (in this case
|
|
* using an authorization server as an intermediary). The authorization server
|
|
* conducts an approval dialog with the user to obtain permission. After access
|
|
* has been allowed, a grant is issued to the client which can be exchanged for
|
|
* an access token.
|
|
*
|
|
* This middleware is used to initiate authorization transactions. If a request
|
|
* is parsed and validated, the following properties will be available on the
|
|
* request:
|
|
*
|
|
* req.oauth2.transactionID an ID assigned to this transaction
|
|
* req.oauth2.client client requesting the user's authorization
|
|
* req.oauth2.redirectURI URL to redirect the user to after authorization
|
|
* req.oauth2.req parameters from request made by the client
|
|
*
|
|
* The contents of `req.oauth2.req` depends on the grant type requested by the
|
|
* the client. The `server`'s request parsing functions are used to construct
|
|
* this object, and the application can implement support for these types as
|
|
* necessary, taking advantage of bundled grant middleware.
|
|
*
|
|
* Because the approval dialog may be conducted over a series of requests and
|
|
* responses, a transaction is also stored in the session until a decision is
|
|
* reached. The application is responsible for verifying the user's identity
|
|
* and prompting him or her to allow or deny the request (typically via an HTML
|
|
* form). At that point, `decision` middleware can be utilized to process the
|
|
* user's decision and issue the grant to the client.
|
|
*
|
|
* Callbacks:
|
|
*
|
|
* This middleware requires a `validate` callback, for which the function
|
|
* signature is as follows:
|
|
*
|
|
* function(clientID, redirectURI, done) { ... }
|
|
*
|
|
* `clientID` is the client identifier and `redirectURI` is the redirect URI as
|
|
* indicated by the client. If the request is valid, `done` must be invoked
|
|
* with the following signature:
|
|
*
|
|
* done(err, client, redirectURI);
|
|
*
|
|
* `client` is the client instance which is making the request. `redirectURI`
|
|
* is the URL to which the user will be redirected after authorization is
|
|
* obtained (which may be different, if the server is enforcing registration
|
|
* requirements). If an error occurs, `done` should be invoked with `err` set
|
|
* in idomatic Node.js fashion.
|
|
*
|
|
* Alternate function signatures of the `validate` callback are available if
|
|
* needed. Consult the source code for a definitive reference.
|
|
*
|
|
*
|
|
* Note that authorization may be obtained by the client directly from the user
|
|
* without using an authorization server as an intermediary (for example, when
|
|
* obtaining a grant in the form of the user's password credentials). In these
|
|
* cases, the client interacts only with the token endpoint without any need to
|
|
* interact with the authorization endpoint.
|
|
*
|
|
* Options:
|
|
*
|
|
* idLength length of generated transaction IDs (default: 8)
|
|
* sessionKey key under which transactions are stored in the session (default: 'authorize')
|
|
*
|
|
* Examples:
|
|
*
|
|
* app.get('/dialog/authorize',
|
|
* login.ensureLoggedIn(),
|
|
* server.authorization(function(clientID, redirectURI, done) {
|
|
* Clients.findOne(clientID, function(err, client) {
|
|
* if (err) { return done(err); }
|
|
* if (!client) { return done(null, false); }
|
|
* return done(null, client, client.redirectURI);
|
|
* });
|
|
* }),
|
|
* function(req, res) {
|
|
* res.render('dialog', { transactionID: req.oauth2.transactionID,
|
|
* user: req.user, client: req.oauth2.client });
|
|
* });
|
|
*
|
|
* References:
|
|
* - [Authorization Endpoint](http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-3.1)
|
|
*
|
|
* @param {Server} server
|
|
* @param {Object} options
|
|
* @param {Function} validate
|
|
* @return {Function}
|
|
* @api protected
|
|
*/
|
|
module.exports = function(server, options, validate, immediate, complete) {
|
|
if (typeof options == 'function') {
|
|
complete = immediate;
|
|
immediate = validate;
|
|
validate = options;
|
|
options = undefined;
|
|
}
|
|
options = options || {};
|
|
immediate = immediate || function (client, user, done) { return done(null, false); };
|
|
|
|
if (!server) { throw new TypeError('oauth2orize.authorization middleware requires a server argument'); }
|
|
if (!validate) { throw new TypeError('oauth2orize.authorization middleware requires a validate function'); }
|
|
|
|
var userProperty = options.userProperty || 'user';
|
|
|
|
return function authorization(req, res, next) {
|
|
|
|
var body = req.body || {}
|
|
, type = req.query.response_type || body.response_type;
|
|
|
|
server._parse(type, req, function(err, areq) {
|
|
if (err) { return next(err); }
|
|
if (!areq || !areq.type) { return next(new AuthorizationError('Missing required parameter: response_type', 'invalid_request')); }
|
|
if (areq.type && !areq.clientID) { return next(new AuthorizationError('Unsupported response type: ' + type, 'unsupported_response_type')); }
|
|
|
|
function validated(err, client, redirectURI, webOrigin) {
|
|
// Set properties *before* next()'ing due to error. The presence of a
|
|
// redirectURI being provided, even under error conditions, indicates
|
|
// that the client should be informed of the error via a redirect.
|
|
req.oauth2 = {};
|
|
if (client) { req.oauth2.client = client; }
|
|
if (redirectURI) { req.oauth2.redirectURI = redirectURI; }
|
|
if (webOrigin) { req.oauth2.webOrigin = webOrigin; }
|
|
req.oauth2.req = areq;
|
|
req.oauth2.user = req[userProperty];
|
|
if (req.locals) { req.oauth2.locals = req.locals; }
|
|
|
|
if (err) { return next(err); }
|
|
if (!client) { return next(new AuthorizationError('Unauthorized client', 'unauthorized_client')); }
|
|
|
|
function immediated(err, allow, info, locals) {
|
|
if (err) { return next(err); }
|
|
if (allow) {
|
|
req.oauth2.res = info || {};
|
|
req.oauth2.res.allow = true;
|
|
if (locals) {
|
|
req.oauth2.locals = req.oauth2.locals || {};
|
|
utils.merge(req.oauth2.locals, locals);
|
|
}
|
|
|
|
function completing(cb) {
|
|
if (!complete) { return cb(); }
|
|
complete(req, req.oauth2, cb);
|
|
}
|
|
|
|
server._respond(req.oauth2, res, completing, function(err) {
|
|
if (err) { return next(err); }
|
|
return next(new AuthorizationError('Unsupported response type: ' + req.oauth2.req.type, 'unsupported_response_type'));
|
|
});
|
|
} else {
|
|
// Add info and locals to `req.oauth2`, where they will be
|
|
// available to the next middleware. Since this is a
|
|
// non-immediate response, the next middleware's responsibility is
|
|
// to prompt the user to allow or deny access. `info` and
|
|
// `locals` are passed along as they may be of assistance when
|
|
// rendering the prompt.
|
|
//
|
|
// Note that, when using the legacy transaction store, `info` is
|
|
// also serialized into the transaction, where it can further be
|
|
// utilized in the `decision` middleware after the user submits the
|
|
// prompt's form. As such, `info` should be a normal JSON object,
|
|
// so that it can be correctly serialized into the session.
|
|
// `locals` is only carried through to the middleware chain for the
|
|
// current request, so it may contain instantiated classes that
|
|
// don't serialize cleanly.
|
|
//
|
|
// The transaction store is pluggable when initializing the `Server`
|
|
// instance. If an application implements a custom transaction
|
|
// store, the specific details of what properties are serialized
|
|
// into the transaction and loaded on subsequent requests are
|
|
// determined by the implementation.
|
|
req.oauth2.info = info;
|
|
if (locals) {
|
|
req.oauth2.locals = req.oauth2.locals || {};
|
|
utils.merge(req.oauth2.locals, locals);
|
|
}
|
|
|
|
// A dialog needs to be conducted to obtain the user's approval.
|
|
// Serialize a transaction to the session. The transaction will be
|
|
// restored (and removed) from the session when the user allows or
|
|
// denies the request.
|
|
function stored(err, tid) {
|
|
if (err) { return next(err); }
|
|
req.oauth2.transactionID = tid;
|
|
next();
|
|
}
|
|
|
|
if (server._txnStore.legacy == true) {
|
|
var txn = {};
|
|
txn.protocol = 'oauth2';
|
|
txn.client = client;
|
|
txn.redirectURI = redirectURI;
|
|
txn.webOrigin = webOrigin;
|
|
txn.req = areq;
|
|
txn.info = info;
|
|
|
|
server._txnStore.store(server, options, req, txn, stored);
|
|
} else {
|
|
server._txnStore.store(req, req.oauth2, stored);
|
|
}
|
|
}
|
|
}
|
|
|
|
var arity = immediate.length;
|
|
if (arity == 7) {
|
|
immediate(req.oauth2.client, req.oauth2.user, req.oauth2.req.scope, req.oauth2.req.type, req.oauth2.req, req.oauth2.locals, immediated);
|
|
} else if (arity == 6) {
|
|
immediate(req.oauth2.client, req.oauth2.user, req.oauth2.req.scope, req.oauth2.req.type, req.oauth2.req, immediated);
|
|
} else if (arity == 5) {
|
|
immediate(req.oauth2.client, req.oauth2.user, req.oauth2.req.scope, req.oauth2.req.type, immediated);
|
|
} else if (arity == 4) {
|
|
immediate(req.oauth2.client, req.oauth2.user, req.oauth2.req.scope, immediated);
|
|
} else if (arity == 3) {
|
|
immediate(req.oauth2.client, req.oauth2.user, immediated);
|
|
} else { // arity == 2
|
|
immediate(req.oauth2, immediated);
|
|
}
|
|
}
|
|
|
|
try {
|
|
var arity = validate.length;
|
|
if (arity == 5) {
|
|
validate(areq.clientID, areq.redirectURI, areq.scope, areq.type, validated);
|
|
} else if (arity == 4) {
|
|
validate(areq.clientID, areq.redirectURI, areq.scope, validated);
|
|
} else if (arity == 3) {
|
|
validate(areq.clientID, areq.redirectURI, validated);
|
|
} else { // arity == 2
|
|
validate(areq, validated);
|
|
}
|
|
} catch (ex) {
|
|
return next(ex);
|
|
}
|
|
});
|
|
};
|
|
};
|