loopback-component-oauth2
Advanced tools
Comparing version 2.0.0-beta4 to 2.0.0-beta5
@@ -0,4 +1,20 @@ | ||
2015-03-10, Version 2.0.0-beta5 | ||
=============================== | ||
* Add tokenTypes (Raymond Feng) | ||
* Return anonymous user (Raymond Feng) | ||
* Tidy up application validation (Raymond Feng) | ||
* Add permission management and subject support for client credentials (Raymond Feng) | ||
* Pull in changes from upstream (Raymond Feng) | ||
2015-01-30, Version 2.0.0-beta4 | ||
=============================== | ||
* v2.0.0-beta4 (Raymond Feng) | ||
* Make sure bodyParser doesn't interfere with proxy (Raymond Feng) | ||
@@ -5,0 +21,0 @@ |
@@ -6,11 +6,9 @@ { | ||
"type": "string", | ||
"index": { | ||
"unique": true | ||
} | ||
"id": true, | ||
"generated": false | ||
}, | ||
"description": "string", | ||
"iconURL": "string", | ||
"ttl": "number", | ||
"requiredAccessLevel": "number" | ||
"ttl": "number" | ||
} | ||
} |
var util = require('util'); | ||
/** | ||
* Module dependencies. | ||
*/ | ||
var OAuth2Error = require('./oauth2error'); | ||
/** | ||
* `AuthorizationError` error. | ||
@@ -20,15 +25,11 @@ * | ||
Error.call(this); | ||
OAuth2Error.call(this, message, code, uri, status); | ||
Error.captureStackTrace(this, arguments.callee); | ||
this.name = 'AuthorizationError'; | ||
this.message = message; | ||
this.code = code || 'server_error'; | ||
this.uri = uri; | ||
this.status = status || 500; | ||
} | ||
/** | ||
* Inherit from `Error`. | ||
* Inherit from `OAuth2Error`. | ||
*/ | ||
util.inherits(AuthorizationError, Error); | ||
util.inherits(AuthorizationError, OAuth2Error); | ||
@@ -35,0 +36,0 @@ /** |
var util = require('util'); | ||
/** | ||
* Module dependencies. | ||
*/ | ||
var OAuth2Error = require('./oauth2error'); | ||
/** | ||
* `TokenError` error. | ||
@@ -19,15 +24,11 @@ * | ||
Error.call(this); | ||
OAuth2Error.call(this, message, code, uri, status); | ||
Error.captureStackTrace(this, arguments.callee); | ||
this.name = 'TokenError'; | ||
this.message = message; | ||
this.code = code || 'server_error'; | ||
this.uri = uri; | ||
this.status = status || 500; | ||
} | ||
/** | ||
* Inherit from `Error`. | ||
* Inherit from `OAuth2Error`. | ||
*/ | ||
util.inherits(TokenError, Error); | ||
util.inherits(TokenError, OAuth2Error); | ||
@@ -34,0 +35,0 @@ /** |
@@ -101,4 +101,6 @@ /** | ||
if (err) { return next(err); } | ||
if (!accessToken) { return next(new TokenError('Invalid client credentials', 'invalid_grant')); } | ||
if (refreshToken && typeof refreshToken == 'object') { | ||
if (!accessToken) { | ||
return next(new TokenError('Invalid client credentials', 'invalid_grant')); | ||
} | ||
if (refreshToken && typeof refreshToken === 'object') { | ||
params = refreshToken; | ||
@@ -123,3 +125,7 @@ refreshToken = null; | ||
var arity = issue.length; | ||
if (arity === 3) { | ||
if (arity === 4) { | ||
// Allow subject (username or email) to be specified | ||
var subject = req.body.sub || req.body.subject || req.body.username; | ||
issue(client, subject, scope, issued); | ||
} else if (arity === 3) { | ||
issue(client, scope, issued); | ||
@@ -126,0 +132,0 @@ } else { // arity == 2 |
@@ -5,2 +5,3 @@ /** | ||
var utils = require('../utils') | ||
, helpers = require('../oauth2-helper') | ||
, AuthorizationError = require('../errors/authorizationerror'); | ||
@@ -100,3 +101,3 @@ | ||
module.exports = function(server, options, validate, immediate) { | ||
if (typeof options == 'function') { | ||
if (typeof options === 'function') { | ||
immediate = validate; | ||
@@ -155,33 +156,14 @@ validate = options; | ||
var validURIs = client.callbackUrls || client.redirectUris || []; | ||
if (validURIs) { | ||
if (typeof validURIs === 'string') { | ||
validURIs = [validURIs]; | ||
} | ||
} | ||
var valid = true; | ||
validURIs = helpers.normalizeList(validURIs); | ||
if (!redirectURI) { | ||
redirectURI = validURIs[0]; | ||
} else { | ||
for (var i = 0, n = validURIs.length; i < n; i++) { | ||
valid = false; | ||
if (redirectURI.indexOf(validURIs[i]) === 0) { | ||
valid = true; | ||
break; | ||
} | ||
} | ||
} | ||
if (!valid) { | ||
// The redirect_uri doesn't match pre-registered ones | ||
return next(new AuthorizationError( | ||
'Invalid request: redirect_uri "' + | ||
redirectURI + | ||
'" is invalid', | ||
'invalid_request')); | ||
} | ||
if (!redirectURI) { | ||
// The redirect uri is missing | ||
return next(new AuthorizationError( | ||
'Invalid request: redirect_uri is missing', | ||
'Invalid request: redirect_uri is missing', | ||
'invalid_request')); | ||
} | ||
helpers.validateClient(client, {redirectURI: redirectURI}, next); | ||
req.oauth2.redirectURI = redirectURI; | ||
@@ -192,6 +174,6 @@ | ||
function immediated(err, allow, ares) { | ||
function immediated(err, allow, info, locals) { | ||
if (err) { return next(err); } | ||
if (allow) { | ||
req.oauth2.res = ares || {}; | ||
req.oauth2.res = info || {}; | ||
req.oauth2.res.allow = true; | ||
@@ -213,2 +195,18 @@ | ||
req.oauth2.transactionID = tid; | ||
// 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 `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. | ||
req.oauth2.info = info; | ||
req.oauth2.locals = locals; | ||
@@ -220,2 +218,3 @@ var txn = {}; | ||
txn.req = areq; | ||
txn.info = info; | ||
// store transaction in session | ||
@@ -222,0 +221,0 @@ var txns = req.session[key] = req.session[key] || {}; |
@@ -91,3 +91,3 @@ /** | ||
var enc = 'query'; | ||
if (req.oauth2 && req.oauth2.req) { | ||
if (req.oauth2.req) { | ||
var type = new UnorderedList(req.oauth2.req.type); | ||
@@ -94,0 +94,0 @@ // In accordance with [OAuth 2.0 Multiple Response Type Encoding |
@@ -62,2 +62,3 @@ /** | ||
req.oauth2.req = txn.req; | ||
req.oauth2.info = txn.info; | ||
next(); | ||
@@ -64,0 +65,0 @@ }); |
var debug = require('debug')('loopback:oauth2:models'); | ||
var helpers = require('../oauth2-helper'); | ||
/** | ||
@@ -10,5 +11,2 @@ * Create oAuth 2.0 metadata models | ||
options = options || {}; | ||
var userModel = options.userModel || loopback.getModelByType(loopback.User); | ||
var applicationModel = options.applicationModel | ||
|| loopback.getModelByType(loopback.Application); | ||
@@ -21,4 +19,10 @@ var dataSource = options.dataSource; | ||
var oauth2 = require('./oauth2-models')(dataSource); | ||
var userModel = options.userModel || loopback.getModelByType(loopback.User); | ||
var applicationModel = options.applicationModel | ||
|| loopback.getModelByType(loopback.Application); | ||
var oAuthTokenModel = oauth2.OAuthToken; | ||
var oAuthAuthorizationCodeModel = oauth2.OAuthAuthorizationCode; | ||
var oAuthPermissionModel = oauth2.OAuthPermission; | ||
@@ -37,2 +41,8 @@ oAuthTokenModel.belongsTo(userModel, | ||
oAuthPermissionModel.belongsTo(userModel, | ||
{as: 'user', foreignKey: 'userId'}); | ||
oAuthPermissionModel.belongsTo(applicationModel, | ||
{as: 'application', foreignKey: 'appId'}); | ||
var getTTL = typeof options.getTTL === 'function' ? options.getTTL : | ||
@@ -110,12 +120,24 @@ function(grantType, clientId, resourceOwner, scopes) { | ||
token.save = function(token, clientId, resourceOwner, scopes, refreshToken, done) { | ||
var tokenObj; | ||
if (arguments.length === 2 && typeof token === 'object') { | ||
// save(token, cb) | ||
tokenObj = token; | ||
done = clientId; | ||
} | ||
var ttl = getTTL('token', clientId, resourceOwner, scopes); | ||
oAuthTokenModel.create({ | ||
id: token, | ||
appId: clientId, | ||
userId: resourceOwner, | ||
scopes: scopes, | ||
issuedAt: new Date(), | ||
expiresIn: ttl, | ||
refreshToken: refreshToken | ||
}, done); | ||
if (!tokenObj) { | ||
tokenObj = { | ||
id: token, | ||
appId: clientId, | ||
userId: resourceOwner, | ||
scopes: scopes, | ||
issuedAt: new Date(), | ||
expiresIn: ttl, | ||
refreshToken: refreshToken | ||
}; | ||
} | ||
tokenObj.expiresIn = ttl; | ||
tokenObj.issuedAt = new Date(); | ||
tokenObj.expiredAt = new Date(tokenObj.issuedAt.getTime() + ttl * 1000); | ||
oAuthTokenModel.create(tokenObj, done); | ||
}; | ||
@@ -131,12 +153,73 @@ | ||
code.save = function(code, clientId, redirectURI, resourceOwner, scopes, done) { | ||
var codeObj; | ||
if (arguments.length === 2 && typeof token === 'object') { | ||
// save(code, cb) | ||
codeObj = code; | ||
done = clientId; | ||
} | ||
var ttl = getTTL('code', clientId, resourceOwner, scopes); | ||
oAuthAuthorizationCodeModel.create({ | ||
id: code, | ||
if (!codeObj) { | ||
codeObj = { | ||
id: code, | ||
appId: clientId, | ||
userId: resourceOwner, | ||
scopes: scopes, | ||
redirectURI: redirectURI | ||
}; | ||
} | ||
codeObj.expiresIn = ttl; | ||
codeObj.issuedAt = new Date(); | ||
codeObj.expiredAt = new Date(codeObj.issuedAt.getTime() + ttl * 1000); | ||
oAuthAuthorizationCodeModel.create(codeObj, done); | ||
}; | ||
var oAuthPermissionModel = oauth2.OAuthPermission; | ||
var permission = {}; | ||
permission.find = function(appId, userId, done) { | ||
oAuthPermissionModel.findOne({where: { | ||
appId: appId, | ||
userId: userId | ||
}}, done); | ||
}; | ||
/* | ||
* Check if a client app is authorized by the user | ||
*/ | ||
permission.isAuthorized = function(appId, userId, scopes, done) { | ||
permission.find(appId, userId, function(err, perm) { | ||
if (err) { | ||
return done(err); | ||
} | ||
if (!perm) { | ||
return done(null, false); | ||
} | ||
var ok = helpers.isScopeAuthorized(scopes, perm.scopes); | ||
var info = ok ? { authorized: true} : {}; | ||
return done(null, ok, info); | ||
}); | ||
}; | ||
/* | ||
* Grant permissions to a client app by a user | ||
*/ | ||
permission.addPermission = function(appId, userId, scopes, done) { | ||
oAuthPermissionModel.findOrCreate({where: { | ||
appId: appId, | ||
userId: userId | ||
}}, { | ||
appId: appId, | ||
userId: userId, | ||
scopes: scopes, | ||
redirectURI: redirectURI, | ||
userId: resourceOwner, | ||
appId: clientId, | ||
issuedAt: new Date(), | ||
expiresIn: ttl | ||
}, done); | ||
issuedAt: new Date() | ||
}, function(err, perm, created) { | ||
if (created) { | ||
return done(err, perm, created); | ||
} else { | ||
if (helpers.isScopeAuthorized(scopes, perm.scopes)) { | ||
return done(err, perm); | ||
} else { | ||
perm.updateAttributes({scopes: helpers.normalizeList(scopes)}, done); | ||
} | ||
} | ||
}); | ||
}; | ||
@@ -149,3 +232,4 @@ | ||
accessTokens: token, | ||
authorizationCodes: code | ||
authorizationCodes: code, | ||
permissions: permission | ||
}; | ||
@@ -152,0 +236,0 @@ |
var tokenDef = require('../../common/models/oauth-token.json'); | ||
var authorizationCodeDef = | ||
require('../../common/models/oauth-authorization-code.json'); | ||
var clientRegistrationDef = | ||
require('../../common/models/oauth-client-registration.json'); | ||
var clientApplicationDef = | ||
require('../../common/models/oauth-client-application.json'); | ||
var permissionDef = | ||
@@ -14,2 +14,14 @@ require('../../common/models/oauth-permission.json'); | ||
// Remove proerties that will confuse LB | ||
function getSettings(def) { | ||
var settings = {}; | ||
for (var s in def) { | ||
if (s === 'name' || s === 'properties') { | ||
continue; | ||
} else { | ||
settings[s] = def[s]; | ||
} | ||
} | ||
} | ||
module.exports = function(dataSource) { | ||
@@ -19,23 +31,33 @@ | ||
var OAuthToken = dataSource.createModel( | ||
tokenDef.name, tokenDef.properties); | ||
tokenDef.name, tokenDef.properties, getSettings(tokenDef)); | ||
// "OAuth authorization code" | ||
var OAuthAuthorizationCode = dataSource.createModel( | ||
authorizationCodeDef.name, authorizationCodeDef.properties); | ||
authorizationCodeDef.name, | ||
authorizationCodeDef.properties, | ||
getSettings(authorizationCodeDef)); | ||
// "OAuth client registration record" | ||
var ClientRegistration = dataSource.createModel( | ||
clientRegistrationDef.name, clientRegistrationDef.properties); | ||
var OAuthClientApplication = dataSource.createModel( | ||
clientApplicationDef.name, | ||
clientApplicationDef.properties, | ||
getSettings(clientApplicationDef)); | ||
// "OAuth permission" | ||
var OAuthPermission = dataSource.createModel( | ||
permissionDef.name, permissionDef.properties); | ||
permissionDef.name, | ||
permissionDef.properties, | ||
getSettings(permissionDef)); | ||
// "OAuth scope" | ||
var OAuthScope = dataSource.createModel( | ||
scopeDef.name, scopeDef.properties); | ||
scopeDef.name, | ||
scopeDef.properties, | ||
scopeDef); | ||
// "OAuth scope mapping" | ||
var OAuthScopeMapping = dataSource.createModel( | ||
scopeMappingDef.name, scopeMappingDef.properties); | ||
scopeMappingDef.name, | ||
scopeMappingDef.properties, | ||
getSettings(scopeMappingDef)); | ||
@@ -45,3 +67,3 @@ return { | ||
OAuthAuthorizationCode: OAuthAuthorizationCode, | ||
ClientRegistration: ClientRegistration, | ||
OAuthClientApplication: OAuthClientApplication, | ||
OAuthPermission: OAuthPermission, | ||
@@ -48,0 +70,0 @@ OAuthScope: OAuthScope, |
@@ -5,7 +5,7 @@ /** | ||
var url = require('url') | ||
, async = require('async') | ||
, oauth2Provider = require('./oauth2orize') | ||
, scopeValidator = require('./scope') | ||
, TokenError = require('./errors/tokenerror') | ||
, AuthorizationError = require('./errors/authorizationerror') | ||
, utils = require('./utils') | ||
, helpers = require('./oauth2-helper') | ||
, modelBuilder = require('./models/index') | ||
@@ -17,176 +17,12 @@ , debug = require('debug')('loopback:oauth2') | ||
, BasicStrategy = require('passport-http').BasicStrategy | ||
, ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy | ||
, BearerStrategy = require('passport-http-bearer').Strategy | ||
, ClientJWTBearerStrategy = require('./strategy/jwt-bearer').Strategy; | ||
, ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy; | ||
function clientInfo(client) { | ||
if (!client) { | ||
return client; | ||
} | ||
return client.id + ',' + client.name; | ||
} | ||
var clientInfo = helpers.clientInfo; | ||
var userInfo = helpers.userInfo; | ||
var isExpired = helpers.isExpired; | ||
var validateClient = helpers.validateClient; | ||
function userInfo(user) { | ||
if (!user) { | ||
return user; | ||
} | ||
return user.id + ',' + user.username + ',' + user.email; | ||
} | ||
var setupResourceServer = require('./resource-server'); | ||
function isExpired(tokenOrCode) { | ||
var issuedTime = | ||
(tokenOrCode.issuedAt && tokenOrCode.issuedAt.getTime()) || -1; | ||
var now = Date.now(); | ||
var expirationTime = | ||
(tokenOrCode.expiredAt && tokenOrCode.expiredAt.getTime()) || -1; | ||
if (expirationTime === -1 && issuedTime !== -1 && | ||
typeof tokenOrCode.expiresIn === 'number') { | ||
expirationTime = issuedTime + tokenOrCode.expiresIn * 1000; | ||
} | ||
return now > expirationTime; | ||
} | ||
/** | ||
* Set up oAuth 2.0 strategies | ||
* @param {Object} app App instance | ||
* @param {Object} options Options | ||
* @param {Object} models oAuth 2.0 metadata models | ||
* @param {Boolean} jwt if jwt-bearer should be enabled | ||
* @returns {Function} | ||
*/ | ||
function setupResourceServer(app, options, models, jwt) { | ||
/** | ||
* BearerStrategy | ||
* | ||
* This strategy is used to authenticate users based on an access token (aka a | ||
* bearer token). The user must have previously authorized a client | ||
* application, which is issued an access token to make requests on behalf of | ||
* the authorizing user. | ||
*/ | ||
passport.use(new BearerStrategy({passReqToCallback: true}, | ||
function(req, accessToken, done) { | ||
debug('Verifying access token %s', accessToken); | ||
models.accessTokens.find(accessToken, function(err, token) { | ||
if (err || !token) { | ||
return done(err); | ||
} | ||
debug('Access token found: %j', token); | ||
if (isExpired(token)) { | ||
return done(new TokenError('Access token is expired', | ||
'invalid_grant')); | ||
} | ||
var userId = token.userId || token.resourceOwner; | ||
var appId = token.appId || token.clientId; | ||
var user, app; | ||
async.parallel([ | ||
function lookupUser(done) { | ||
if (userId == null) { | ||
return process.nextTick(done); | ||
} | ||
models.users.find(userId, function(err, u) { | ||
if (err) { | ||
return done(err); | ||
} | ||
if (!u) { | ||
return done( | ||
new TokenError('Access token has invalid user id: ' + | ||
userId, 'invalid_grant')); | ||
} | ||
debug('User found: %s', userInfo(u)); | ||
user = u; | ||
done(); | ||
}); | ||
}, | ||
function lookupApp(done) { | ||
if (appId == null) { | ||
return process.nextTick(done); | ||
} | ||
models.clients.find(appId, function(err, a) { | ||
if (err) { | ||
return done(err); | ||
} | ||
if (!a) { | ||
return done( | ||
new TokenError('Access token has invalid app id: ' + appId, | ||
'invalid_grant')); | ||
} | ||
debug('Client found: %s', clientInfo(a)); | ||
app = a; | ||
done(); | ||
}); | ||
}], function(err) { | ||
if (err) { | ||
return done(err); | ||
} | ||
var authInfo = { accessToken: token, user: user, app: app }; | ||
req.accessToken = token; | ||
done(null, user, authInfo); | ||
}); | ||
}); | ||
}) | ||
); | ||
/** | ||
* JWT bearer token | ||
*/ | ||
if (jwt) { | ||
passport.use('oauth2-jwt-bearer', new ClientJWTBearerStrategy( | ||
{audience: options.tokenPath || '/oauth/token', passReqToCallback: true}, | ||
function(req, iss, header, done) { | ||
debug('Looking up public key for %s', iss); | ||
models.clients.findByClientId(iss, function(err, client) { | ||
if (err) { | ||
return done(err); | ||
} | ||
if (!client) { | ||
return done(null, false); | ||
} | ||
return done(null, client.publicKey); | ||
}); | ||
}, | ||
function(req, iss, header, done) { | ||
models.clients.findByClientId(iss, function(err, client) { | ||
if (err) { | ||
return done(err); | ||
} | ||
if (!client) { | ||
return done(null, false); | ||
} | ||
return done(null, client); | ||
}); | ||
} | ||
)); | ||
} | ||
/** | ||
* Return the middleware chain to enfore oAuth 2.0 authentication and | ||
* authorization | ||
* @param {Object} [options] Options object | ||
* - scope | ||
* - jwt | ||
*/ | ||
function authenticate(options) { | ||
options = options || {}; | ||
var authenticators = []; | ||
var scopeHandler = scopeValidator(options.scope); | ||
authenticators = [passport.authenticate('bearer', options)]; | ||
if (jwt && options.jwt) { | ||
authenticators.push(passport.authenticate('oauth2-jwt-bearer', options)); | ||
} | ||
if (options.scope) { | ||
authenticators.push(scopeHandler); | ||
} | ||
authenticators.push(oauth2Provider.errorHandler()); | ||
return authenticators; | ||
} | ||
return authenticate; | ||
} | ||
/** | ||
* | ||
@@ -276,21 +112,46 @@ * @param {Object} app The app instance | ||
*/ | ||
var codeGrant; | ||
if (supportedGrantTypes.indexOf('authorizationCode') !== -1) { | ||
server.grant(oauth2Provider.grant.code( | ||
codeGrant = server.grant(oauth2Provider.grant.code( | ||
{ allowsPost: options.allowsPostForAuthorization}, | ||
function(client, redirectURI, user, scope, ares, done) { | ||
var code = generateToken({ | ||
grant: 'Authorization Code', | ||
client: client, | ||
user: user, | ||
if (validateClient(client, { | ||
scope: scope, | ||
redirectURI: redirectURI | ||
}); | ||
redirectURI: redirectURI, | ||
grantType: 'authorization_code' | ||
}, done)) { | ||
return; | ||
} | ||
debug('Generating authorization code: %s %s %s %s %s', | ||
code, clientInfo(client), redirectURI, userInfo(user), scope); | ||
models.authorizationCodes.save(code, client.id, redirectURI, user.id, | ||
scope, | ||
function(err) { | ||
done(err, err ? null : code); | ||
function generateAuthCode() { | ||
var code = generateToken({ | ||
grant: 'Authorization Code', | ||
client: client, | ||
user: user, | ||
scope: scope, | ||
redirectURI: redirectURI | ||
}); | ||
debug('Generating authorization code: %s %s %s %s %s', | ||
code, clientInfo(client), redirectURI, userInfo(user), scope); | ||
models.authorizationCodes.save(code, client.id, redirectURI, | ||
user.id, | ||
scope, | ||
function(err) { | ||
done(err, err ? null : code); | ||
}); | ||
} | ||
if (ares.authorized) { | ||
generateAuthCode(); | ||
} else { | ||
models.permissions.addPermission(client.id, user.id, scope, | ||
function(err) { | ||
if (err) { | ||
return done(err); | ||
} | ||
generateAuthCode(); | ||
}); | ||
} | ||
})); | ||
@@ -392,2 +253,9 @@ | ||
if (validateClient(client, { | ||
scope: scope, | ||
grantType: 'password' | ||
}, done)) { | ||
return; | ||
} | ||
userLogin(username, password, function(err, user) { | ||
@@ -432,25 +300,62 @@ if (err || !user) { | ||
server.exchange(oauth2Provider.exchange.clientCredentials( | ||
function(client, scope, done) { | ||
var token = generateToken({ | ||
grant: 'Client Credentials', | ||
client: client, | ||
scope: scope | ||
}); | ||
debug('Generating access token: %s %s %s', | ||
token, clientInfo(client), scope); | ||
function(client, subject, scope, done) { | ||
var refreshToken = generateToken({ | ||
grant: 'Client Credentials', | ||
client: client, | ||
scope: scope | ||
}); | ||
if (validateClient(client, { | ||
scope: scope, | ||
grantType: 'client_credentials' | ||
}, done)) { | ||
return; | ||
} | ||
models.accessTokens.save(token, client.id, null, scope, refreshToken, | ||
function(err, accessToken) { | ||
done(err, err ? null : token, { | ||
refresh_token: refreshToken, | ||
expires_in: accessToken.expiresIn, | ||
scope: scope && scope.join(' ') | ||
function generateAccessToken(userId) { | ||
var token = generateToken({ | ||
grant: 'Client Credentials', | ||
client: client, | ||
userId: userId, | ||
scope: scope | ||
}); | ||
debug('Generating access token: %s %s %s', | ||
token, clientInfo(client), scope); | ||
var refreshToken = generateToken({ | ||
grant: 'Client Credentials', | ||
client: client, | ||
scope: scope | ||
}); | ||
models.accessTokens.save(token, client.id, userId, scope, refreshToken, | ||
function(err, accessToken) { | ||
done(err, err ? null : token, { | ||
refresh_token: refreshToken, | ||
expires_in: accessToken.expiresIn, | ||
scope: scope && scope.join(' ') | ||
}); | ||
}); | ||
} | ||
if (subject) { | ||
models.users.findByUsernameOrEmail(subject, function(err, user) { | ||
if (err) { | ||
return done(err); | ||
} | ||
if (!user) { | ||
return done(new AuthorizationError( | ||
'Invalid subject: ' + subject, 'access_denied')); | ||
} | ||
models.permissions.isAuthorized(client.id, user.id, scope, | ||
function(err, authorized) { | ||
if (err) { | ||
return done(err); | ||
} | ||
if (authorized) { | ||
generateAccessToken(user.id); | ||
} else { | ||
return done(new AuthorizationError( | ||
'Permission denied by ' + subject, 'access_denied')); | ||
} | ||
}); | ||
}); | ||
} else { | ||
generateAccessToken(); | ||
} | ||
})); | ||
@@ -465,2 +370,10 @@ } | ||
function(client, refreshToken, scope, done) { | ||
if (validateClient(client, { | ||
scope: scope, | ||
grantType: 'refresh_token' | ||
}, done)) { | ||
return; | ||
} | ||
models.accessTokens.findByRefreshToken(refreshToken, | ||
@@ -516,19 +429,42 @@ function(err, accessToken) { | ||
var tokenGrant; | ||
if (supportedGrantTypes.indexOf('implicit') !== -1) { | ||
server.grant(oauth2Provider.grant.token( | ||
tokenGrant = server.grant(oauth2Provider.grant.token( | ||
{ allowsPost: options.allowsPostForAuthorization}, | ||
function(client, user, scope, ares, done) { | ||
var token = generateToken({ | ||
grant: 'Implicit', | ||
client: client, | ||
user: user, | ||
scope: scope | ||
}); | ||
debug('Generating access token: %s %s %s %s', | ||
token, clientInfo(client), userInfo(user), scope); | ||
models.accessTokens.save(token, client.id, user.id, scope, null, | ||
function(err) { | ||
done(err, err ? null : token); | ||
if (validateClient(client, { | ||
scope: scope, | ||
grantType: 'implicit' | ||
}, done)) { | ||
return; | ||
} | ||
function generateAccessToken() { | ||
var token = generateToken({ | ||
grant: 'Implicit', | ||
client: client, | ||
user: user, | ||
scope: scope | ||
}); | ||
debug('Generating access token: %s %s %s %s', | ||
token, clientInfo(client), userInfo(user), scope); | ||
models.accessTokens.save(token, client.id, user.id, scope, null, | ||
function(err) { | ||
done(err, err ? null : token); | ||
}); | ||
} | ||
if (ares.authorized) { | ||
generateAccessToken(); | ||
} else { | ||
models.permissions.addPermission(client.id, user.id, scope, | ||
function(err) { | ||
if (err) { | ||
return done(err); | ||
} | ||
generateAccessToken(); | ||
}); | ||
} | ||
})); | ||
@@ -558,16 +494,50 @@ } | ||
var payload = JSON.parse(decodedJWT.payload); | ||
// payload.iss == client.id | ||
var token = generateToken({ | ||
grant: 'JWT', | ||
client: client, | ||
claims: payload | ||
}); | ||
debug('Generating access token %s %s %s', token, | ||
clientInfo(client), jwtToken); | ||
// FIXME: [rfeng] Map payload.sub to userId | ||
// Check OAuthPermission model to see if it's pre-approved | ||
models.accessTokens.save(token, client.id, null, payload.scope, null, | ||
function(err) { | ||
done(err, err ? null : token); | ||
if (validateClient(client, { | ||
scope: payload.scope, | ||
grantType: 'urn:ietf:params:oauth:grant-type:jwt-bearer' | ||
}, done)) { | ||
return; | ||
} | ||
function generateAccessToken(userId) { | ||
var token = generateToken({ | ||
grant: 'JWT', | ||
client: client, | ||
claims: payload | ||
}); | ||
debug('Generating access token %s %s %s', token, | ||
clientInfo(client), jwtToken); | ||
// Check OAuthPermission model to see if it's pre-approved | ||
models.accessTokens.save(token, client.id, userId, payload.scope, null, | ||
function(err) { | ||
done(err, err ? null : token); | ||
}); | ||
} | ||
if (payload.sub) { | ||
models.users.findByUsernameOrEmail(payload.sub, function(err, user) { | ||
if (err) { | ||
return done(err); | ||
} | ||
if (!user) { | ||
return done(new AuthorizationError( | ||
'Invalid subject: ' + payload.sub, 'access_denied')); | ||
} | ||
models.permissions.isAuthorized(client.id, user.id, payload.scope, | ||
function(err, authorized) { | ||
if (err) { | ||
return done(err); | ||
} | ||
if (authorized) { | ||
generateAccessToken(user.id); | ||
} else { | ||
done(new AuthorizationError( | ||
'Permission denied by ' + payload.sub), 'access_denied'); | ||
} | ||
}); | ||
}); | ||
} else { | ||
generateAccessToken(); | ||
} | ||
})); | ||
@@ -594,37 +564,52 @@ } | ||
handlers.authorization = [ | ||
server.authorization(function(clientID, redirectURI, done) { | ||
debug('Verifying client %s %s', clientID, redirectURI); | ||
models.clients.findByClientId(clientID, function(err, client) { | ||
if (err || !client) { | ||
return done(err); | ||
} | ||
debug('Client found: %s', clientInfo(client)); | ||
var redirectURIs = []; | ||
if (typeof client.redirectURI === 'string') { | ||
redirectURIs.push(client.redirectURI); | ||
} | ||
if (Array.isArray(client.redirectURIs)) { | ||
redirectURIs = redirectURIs.concat(client.redirectURIs); | ||
} | ||
debug('Checking redirect URIs %j', redirectURIs); | ||
if (redirectURIs.length === 0) { | ||
return done(null, client, redirectURI); | ||
} else { | ||
var matched = false; | ||
for (var i = 0, n = redirectURIs.length; i < n; i++) { | ||
if (redirectURI.indexOf(redirectURIs[i]) === 0) { | ||
matched = true; | ||
break; | ||
} | ||
} | ||
if (!matched) { | ||
err = new Error('Invalid redirectURI: ' + redirectURI); | ||
server.authorization( | ||
function(clientID, redirectURI, scope, responseType, done) { | ||
debug('Verifying client %s redirect-uri: %s scope: %s response-type: %s', | ||
clientID, redirectURI, scope, responseType); | ||
models.clients.findByClientId(clientID, function(err, client) { | ||
if (err || !client) { | ||
return done(err); | ||
} | ||
debug('Client found: %s', clientInfo(client)); | ||
if (validateClient(client, { | ||
scope: scope, | ||
redirectURI: redirectURI, | ||
responseType: responseType | ||
}, done)) { | ||
return; | ||
} | ||
return done(null, client, redirectURI); | ||
} | ||
}); | ||
}), | ||
}); | ||
}), | ||
// Ensure the user is logged in | ||
login.ensureLoggedIn({ redirectTo: options.loginPage || '/login' }), | ||
// Check if the user has granted permissions to the client app | ||
function(req, res, next) { | ||
if (options.forceAuthorize) { | ||
return next(); | ||
} | ||
var userId = req.oauth2.user.id; | ||
var clientId = req.oauth2.client.id; | ||
var scope = req.oauth2.req.scope; | ||
models.permissions.isAuthorized(clientId, userId, scope, | ||
function(err, authorized) { | ||
if (err) { | ||
return next(err); | ||
} else if (authorized) { | ||
req.oauth2.res = {}; | ||
req.oauth2.res.allow = true; | ||
server._respond(req.oauth2, res, function(err) { | ||
if (err) { | ||
return next(err); | ||
} | ||
return next(new AuthorizationError('Unsupported response type: ' | ||
+ req.oauth2.req.type, 'unsupported_response_type')); | ||
}); | ||
} else { | ||
next(); | ||
} | ||
}); | ||
}, | ||
// Now try to render the dialog to approve client app's request for permissions | ||
function(req, res, next) { | ||
if (options.decisionPage) { | ||
@@ -717,8 +702,8 @@ var urlObj = { | ||
var oauth2Paths = [ | ||
options.authorizePath || '/oauth/authorize', | ||
options.tokenPath || '/oauth/token', | ||
options.decisionPath || '/oauth/authorize/decision', | ||
options.loginPath || '/login' | ||
options.authorizePath || '/oauth/authorize', | ||
options.tokenPath || '/oauth/token', | ||
options.decisionPath || '/oauth/authorize/decision', | ||
options.loginPath || '/login' | ||
]; | ||
app.middleware('parse', oauth2Paths, | ||
app.middleware('parse', oauth2Paths, | ||
app.loopback.urlencoded({extended: false})); | ||
@@ -725,0 +710,0 @@ app.middleware('parse', oauth2Paths, app.loopback.json({strict: false})); |
@@ -69,4 +69,5 @@ /** | ||
*/ | ||
exports.OAuth2Error = require('./errors/oauth2error'); | ||
exports.AuthorizationError = require('./errors/authorizationerror'); | ||
exports.TokenError = require('./errors/tokenerror'); | ||
var debug = require('debug')('loopback:oauth2:scope'); | ||
var oauth2Provider = require('./oauth2orize'); | ||
var helpers = require('./oauth2-helper'); | ||
/** | ||
* Normalize scope to string[] | ||
* @param {String|String[]} scope | ||
* @returns {String[]} | ||
*/ | ||
function normalizeScope(scope) { | ||
if (!scope) { | ||
return []; | ||
} | ||
var scopes; | ||
if (Array.isArray(scope)) { | ||
scopes = [].concat(scope); | ||
} else if (typeof scope === 'string') { | ||
scopes = scope.split(/[\s,]+/g).filter(Boolean); | ||
} else { | ||
throw new Error('Invalid scope: ' + scope); | ||
} | ||
return scopes; | ||
} | ||
/** | ||
* Check if one of the scopes is in the requiredScopes array | ||
* @param {String[]} requiredScopes An array of required scopes | ||
* @param {String[]} scopes An array of granted scopes | ||
* @returns {boolean} | ||
*/ | ||
function isInScope(requiredScopes, scopes) { | ||
if (requiredScopes.length === 0) { | ||
return true; | ||
} | ||
for (var i = 0, n = requiredScopes.length; i < n; i++) { | ||
if (requiredScopes.indexOf(scopes[i]) !== -1) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
function checkScopes(scopes, cb) { | ||
cb(null, true); | ||
} | ||
module.exports = function(scope) { | ||
var requiredScopes = normalizeScope(scope); | ||
var allowedScopes = scope; | ||
return function validateScope(req, res, next) { | ||
debug('Required scopes: ', requiredScopes); | ||
var scopes = normalizeScope(req.accessToken && req.accessToken.scopes); | ||
debug('Allowed scopes: ', allowedScopes); | ||
var scopes = req.accessToken && req.accessToken.scopes; | ||
debug('Scopes of the access token: ', scopes); | ||
if (isInScope(requiredScopes, scopes)) { | ||
if (helpers.isScopeAllowed(allowedScopes, scopes)) { | ||
next(); | ||
@@ -60,2 +19,2 @@ } else { | ||
}; | ||
} | ||
} |
@@ -183,9 +183,10 @@ // Folked from | ||
arity = self._verify.length; | ||
if (arity === 4) { | ||
if (arity === 5) { | ||
// This variation allows the application to detect the case in which | ||
// the issuer and subject of the assertion are different, and permit | ||
// or deny as necessary. | ||
self._verify(req, payload.iss || header.iss, header, verified); | ||
} else { // arity == 3 | ||
self._verify(req, payload.iss || header.iss, verified); | ||
self._verify(req, payload.iss || header.iss, payload.sub, payload, | ||
verified); | ||
} else { // arity == 4 | ||
self._verify(req, payload.iss || header.iss, payload.sub, verified); | ||
} | ||
@@ -192,0 +193,0 @@ } else { |
{ | ||
"name": "loopback-component-oauth2", | ||
"version": "2.0.0-beta4", | ||
"version": "2.0.0-beta5", | ||
"description": "OAuth 2.0 provider for LoopBack", | ||
@@ -29,4 +29,4 @@ "keywords": [ | ||
"connect-ensure-login": "^0.1.1", | ||
"debug": "^2.1.0", | ||
"jws": "^1.0.0", | ||
"debug": "^2.1.2", | ||
"jws": "^2.0.0", | ||
"passport": "^0.2.1", | ||
@@ -43,4 +43,4 @@ "passport-http": "^0.2.2", | ||
"devDependencies": { | ||
"mocha": "^2.0.1", | ||
"chai": "^1.10.0", | ||
"mocha": "^2.1.0", | ||
"chai": "^2.1.0", | ||
"chai-connect-middleware": "^0.3.1", | ||
@@ -47,0 +47,0 @@ "chai-oauth2orize-grant": "^0.2.0", |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
457075
46
4011
+ Addedjws@2.0.0(transitive)
- Removedjws@1.0.1(transitive)
Updateddebug@^2.1.2
Updatedjws@^2.0.0