diff --git a/lib/auth/index.js b/lib/auth/index.js new file mode 100755 index 000000000..cbafc516a --- /dev/null +++ b/lib/auth/index.js @@ -0,0 +1,41 @@ +// Load modules + +var Oz = require('./oz'); +var Utils = require('../utils'); +var Err = require('../error'); +var Log = require('../log'); + + +// Declare internals + +var internals = {}; + + +exports = module.exports = internals.Auth = function (server, options) { + + Utils.assert(this.constructor === internals.Auth, 'Auth must be instantiated using new'); + Utils.assert(options, 'Invalid options'); + Utils.assert(options.scheme, 'Missing scheme'); + + // Built-in schemes + + if (options.scheme === 'oz') { + this.scheme = new Oz.Scheme(server, options); + } + else if (options.scheme === 'basic') { + + } + else { + + } + + Log.event(['info', 'config', 'auth'], server.settings.nickname + ': Authentication enabled'); + return this; +}; + + +internals.Auth.prototype.authenticate = function (request, next) { + + return this.scheme.authenticate(request, next); +}; + diff --git a/lib/auth/oz.js b/lib/auth/oz.js index bf6b800fa..0134fc250 100755 --- a/lib/auth/oz.js +++ b/lib/auth/oz.js @@ -1,9 +1,8 @@ // Load modules var Oz = require('oz'); -var Utils = require('./utils'); -var Err = require('./error'); -var Types = require('joi').Types; +var Utils = require('../utils'); +var Err = require('../error'); // Declare internals @@ -11,77 +10,81 @@ var Types = require('joi').Types; var internals = {}; -// Defaults +exports.Scheme = internals.Scheme = function (server, options) { -exports.defaults = { - tokenEndpoint: '/oauth/token', - encryptionPassword: null, - ozSettings: null, + Utils.assert(this.constructor === internals.Scheme, 'Scheme must be instantiated using new'); + Utils.assert(options, 'Invalid options'); + Utils.assert(options.scheme === 'oz', 'Wrong scheme'); + Utils.assert(options.encryptionPassword, 'Missing encryption password'); + Utils.assert(options.loadAppFunc && options.loadGrantFunc, 'Missing required methods in configuration'); - verifyTicketFunc: null, - getAppFunc: null, - checkAuthorizationFunc: null, - extensionFunc: null, + this.settings = Utils.clone(options); // Options can be reused + this.settings.appEndpoint = this.settings.appEndpoint || '/oz/app'; + this.settings.reissueEndpoint = this.settings.reissueEndpoint || '/oz/reissue'; + this.settings.rsvpEndpoint = this.settings.rsvpEndpoint || '/oz/rsvp'; + this.settings.isHttps = !!server.settings.tls; - tos: { - min: 'none' // Format: YYYYMMDD (e.g. '19700101') + // Setup Oz environment + + if (this.settings.ozSettings) { + Oz.settings.set(this.settings.ozSettings); } -}; + // Add protocol endpoints -// Setup endpoints + server.addRoutes([ + { method: 'POST', path: this.settings.appEndpoint, config: this._endpoint('app') }, + { method: 'POST', path: this.settings.reissueEndpoint, config: this._endpoint('reissue') }, + { method: 'POST', path: this.settings.rsvpEndpoint, config: this._endpoint('rsvp') } + ]); -exports.setup = function (server) { + return this; +}; - if (server.settings.auth) { - Utils.assert(server.settings.auth.tokenEndpoint && - server.settings.auth.verifyTicketFunc && - server.settings.auth.getAppFunc && - server.settings.auth.checkAuthorizationFunc && - server.settings.auth.encryptionPassword, 'Invalid authentication configuration'); - if (server.settings.auth.ozSettings) { - Oz.settings.set(server.settings.auth.ozSettings); - } +// Request an applicaiton ticket using Basic authentication - server.addRoute({ - method: 'POST', - path: server.settings.auth.tokenEndpoint, - config: exports.token - }); +internals.Scheme.prototype._endpoint = function (name) { - Log.event(['info', 'config'], server.settings.nickname + ': Authentication enabled'); - } -}; + var self = this; + var endpoint = { + auth: { + mode: 'none' + }, + handler: function (request) { -// Token Authentication + Oz.endpoints[name](request.raw.req, request.payload, self.settings, function (err, response) { + + if (err && + err.wwwAuthenticateHeader) { + + request.raw.res.setHeader('WWW-Authenticate', err.wwwAuthenticateHeader); + } -exports.authenticate = function (request, next) { + return request.reply(err || response); + }); + } + }; - /* + return endpoint; +}; - if (this.config.auth.mode !== 'none') { - this.config.auth.scope = this.config.auth.scope || null; - this.config.auth.tos = this.config.auth.tos || this.server.settings.auth.tos.min; - this.config.auth.entity = this.config.auth.entity || 'user'; - Utils.assert(['user', 'app', 'any'].indexOf(this.config.auth.entity) !== -1, 'Unknown authentication entity: ' + this.config.auth.entity); - } - - */ +// Token Authentication +internals.Scheme.prototype.authenticate = function (request, next) { - if (request._route.config.auth.mode === 'none') { - return next(); - } + var self = this; var validate = function (err, ticket, attributes) { + var config = request._route.config.auth; + // Unauthenticated if (err) { - if (request._route.config.auth.mode === 'optional' && + if (config.mode === 'optional' && !request.raw.req.headers.authorization) { request.session = null; @@ -100,34 +103,37 @@ exports.authenticate = function (request, next) { // Check scope - if (request._route.config.auth.scope && - request.session.scope.indexOf(request._route.config.auth.scope) === -1) { + if (config.scope && + ticket.scope.indexOf(config.scope) === -1) { - request.log(['auth', 'error', 'scope'], { got: request.session.scope, need: request._route.config.auth.scope }); - return next(Err.forbidden('Insufficient scope (\'' + request._route.config.auth.scope + '\' expected for application ' + request.session.app + ')')); + request.log(['auth', 'error', 'scope'], { got: ticket.scope, need: config.scope }); + return next(Err.forbidden('Insufficient scope (\'' + config.scope + '\' expected for application ' + ticket.app + ')')); } - // User Mode: any + var entity = config.entity || 'user'; - if (request._route.config.auth.entity === 'any') { + // Entity: any + + if (entity === 'any') { request.log(['auth']); return next(); } - // User Mode: required + // Entity: required - if (request._route.config.auth.entity === 'user') { - if (!request.session.user) { + if (entity === 'user') { + if (!ticket.user) { request.log(['auth', 'error'], 'User ticket required'); return next(Err.forbidden('Application ticket cannot be used on a user endpoint')); } // Check TOS - if (request._route.config.auth.tos !== 'none' && - (!request.session.ext || !request.session.ext.tos || request.session.ext.tos < request._route.config.auth.tos)) { + var tos = (config.hasOwnProperty('tos') ? config.tos : self.settings.tos); + if (tos && + (!ticket.ext || !ticket.ext.tos || ticket.ext.tos < tos)) { - request.log(['auth', 'error'], 'Insufficient TOS'); + request.log(['auth', 'error', 'tos'], { min: tos, user: ticket.ext && ticket.ext.tos }); return next(Err.forbidden('Insufficient TOS accepted')); } @@ -135,10 +141,10 @@ exports.authenticate = function (request, next) { return next(); } - // User Mode: none + // Entity: none - if (request._route.config.auth.entity === 'app') { - if (request.session.user) { + if (entity === 'app') { + if (ticket.user) { request.log(['auth', 'error'], 'App ticket required'); return next(Err.forbidden('User ticket cannot be used on an application endpoint')); } @@ -147,149 +153,17 @@ exports.authenticate = function (request, next) { return next(); } - // User Mode: unknown + // Entity: unknown - request.log(['auth', 'error'], 'Unknown entity mode: ' + request._route.config.auth.entity); + request.log(['auth', 'error'], 'Unknown entity mode: ' + entity); return next(Err.internal('Unknown endpoint entity mode')); }; - + if (request.session) { return validate(null, request.session, null); } - Oz.Request.authenticate(request.raw.req, request.server.settings.auth.encryptionPassword, { isHttps: request.server.settings.tls }, validate); -}; - - -// Get session token - -exports.token = { - schema: { - grant_type: Types.String(), - client_id: Types.String().required(), - client_secret: Types.String().emptyOk(), - grant: Types.String() - }, - auth: { - mode: 'optional', - entity: 'any' - }, - handler: function (request) { - - var serverSettings = request.server.settings.auth; - - // Load app information - - if (!request.payload.grant_type) { - return issue(null, app); - } - else if (request.payload.grant_type === 'rsvp') { - - if (!request.payload.grant) { - return request.reply(new Oz.Error('invalid_request', 'Missing grant')); - } - - serverSettings.verifyTicketFunc(request.payload.grant, function (app, user) { - - if (!app || !user) { - return request.reply(new Oz.Error('invalid_grant')); - } - - return issue(user, app); - }); - } - else if (serverSettings.extensionFunc) { - serverSettings.extensionFunc(ticket, function (app, user, action) { - - if (!app || !user) { - return request.reply(new Oz.Error('invalid_grant')); - } - - return issue(user, app, action); - }); - } - else { - // Unsupported grant type - return request.reply(new Oz.Error('unsupported_grant_type', 'Unknown or unsupported grant type')); - } - - - serverSettings.getAppFunc(request.payload.client_id, function (app) { - - if (!app) { - return request.reply(new Oz.Error('invalid_client', 'Invalid application identifier or secret')); - } - - // Check app secret - - if ((app.secret || '') !== (request.payload.client_secret || '')) { - // Bad app authentication - return request.reply(new Oz.Error('invalid_client', 'Invalid application identifier or secret')); - } - - }); - - function issue(user, app, customResponseFields) { - - var generate = function (grant) { - - // Issue a new token - - var ticketAttr = { - app: { - id: app.id, - scope: app.scope - }, - options: {} - }; - - if (user) { - ticketAttr.user = { - id: user.id, - rsvp: user.rsvp - }; - ticketAttr.options.ext = { - tos: user.tos - }; - } - - Oz.Ticket.generate(ticketAttr.app, ticketAttr.user, request.server.settings.auth.encryptionPassword, ticketAttr.options, function (err, ticket) { - - if (err) { - return request.reply(err); - } - - if (user) { - ticket.x_tos = ticketAttr.options.ext.tos - } - - if (grant) { - ticket.rsvp = grant; - } - - Utils.merge(ticket, customResponseFields); - return request.reply(ticket); - }); - }; - - // Application ticket - - if (!user) { - return generate(); - } - - // User ticket - - serverSettings.checkAuthorizationFunc(request.session, app, user, function (err, rsvp) { - - if (err) { - return request.reply(err); - } - - return generate(rsvp); - }); - } - } + Oz.request.authenticate(request.raw.req, this.settings.encryptionPassword, { isHttps: this.settings.isHttps }, validate); }; diff --git a/lib/request.js b/lib/request.js index 9e0396376..c26f0608a 100755 --- a/lib/request.js +++ b/lib/request.js @@ -18,8 +18,6 @@ var Cache = require('./cache'); var internals = {}; -// Create and configure server instance - exports = module.exports = internals.Request = function (server, req, res, options) { var now = Date.now(); // Take measurement as soon as possible @@ -49,7 +47,7 @@ exports = module.exports = internals.Request = function (server, req, res, optio // params // rawBody // payload - // session: { app, scope, user, tos } + // session: { id, app, scope, user, ext.tos } // setUrl() // setMethod() @@ -225,6 +223,7 @@ internals.Request.prototype._execute = function (route) { var funcs = [ // ext.onRequest() in Server internals.processDebug, + internals.authenticate, Validation.query, Payload.read, Validation.payload, @@ -264,6 +263,17 @@ internals.Request.prototype._execute = function (route) { }; +internals.authenticate = function (request, next) { + + var config = request._route.config.auth; + if (config.mode === 'none') { + return next(); + } + + return request.server.auth.authenticate(request, next); +}; + + internals.Request.prototype._decorate = function (callback) { var self = this; diff --git a/lib/server.js b/lib/server.js index 8188a6a98..bd27f03df 100755 --- a/lib/server.js +++ b/lib/server.js @@ -16,6 +16,7 @@ var Route = require('./route'); var Debug = require('./debug'); var Docs = require('./docs'); var Batch = require('./batch'); +var Auth = require('./auth'); // Declare internals @@ -118,6 +119,12 @@ module.exports = internals.Server = function (/* host, port, options */) { this.cache = null; } + // Authentication + + if (this.settings.auth) { + this.auth = new Auth(this, this.settings.auth); + } + // Setup debug endpoint if (this.settings.debug) {