Compare commits

..

No commits in common. "master" and "upstream" have entirely different histories.

9 changed files with 172 additions and 3367 deletions

137
.eslintrc
View File

@ -1,137 +0,0 @@
{
"extends": "airbnb-base",
"parserOptions": {
"sourceType": "script"
},
"rules": {
// Customized
"handle-callback-err": [ "error","^(e$|(e|(.*(_e|E)))rr)" ],
"comma-dangle": ["error", {
"arrays": "always-multiline",
"objects": "always-multiline",
"imports": "always-multiline",
"exports": "always-multiline",
"functions": "never"
}],
"no-empty": ["error", { "allowEmptyCatch": true }],
"no-underscore-dangle": "off",
"no-console": "off",
"no-mixed-operators": ["error", { "allowSamePrecedence": true }],
"strict": ["error", "global"],
"consistent-return": "off",
"func-names": "off",
"no-tabs": "off",
"indent": ["error", "tab"],
"no-eq-null": "off",
"camelcase": "off",
"no-new": "off",
"no-shadow": "off",
"no-use-before-define": ["error", "nofunc"],
"no-prototype-builtins": "off",
"new-cap": "off",
"no-plusplus": ["error", { "allowForLoopAfterthoughts": true }],
"object-curly-newline": "off",
"no-restricted-globals": "off",
"function-paren-newline": "off",
"import/no-unresolved": "error",
"quotes": ["error", "single", {
"avoidEscape": true,
"allowTemplateLiterals": true
}],
"no-else-return": [ "error", { "allowElseIf": true } ],
"operator-linebreak": [ "error", "after" ],
// ES6
"prefer-rest-params": "off",
"prefer-spread": "off",
"prefer-arrow-callback": "off",
"prefer-template": "off",
"no-var": "off",
"object-shorthand": "off",
"vars-on-top": "off",
"prefer-destructuring": "off",
// TODO
"import/no-extraneous-dependencies": "off",
"import/no-dynamic-require": "off",
"import/newline-after-import": "off",
"no-bitwise": "off",
"global-require": "off",
"max-len": "off",
"no-param-reassign": "off",
"no-restricted-syntax": "off",
"no-script-url": "off",
"default-case": "off",
"linebreak-style": "off",
// "no-multi-assign": "off",
// "one-var": "off",
// "no-undef": "off",
// "max-nested-callbacks": "off",
// "no-mixed-requires": "off",
// "brace-style": "off",
// "max-statements-per-line": "off",
// "no-unused-vars": "off",
// "no-mixed-spaces-and-tabs": "off",
// "no-useless-concat": "off",
// "require-jsdoc": "off",
// "eqeqeq": "off",
// "no-negated-condition": "off",
// "one-var-declaration-per-line": "off",
// "no-lonely-if": "off",
// "radix": "off",
// "no-else-return": "off",
// "no-useless-escape": "off",
// "block-scoped-var": "off",
// "operator-assignment": "off",
// "yoda": "off",
// "no-loop-func": "off",
// "no-void": "off",
// "valid-jsdoc": "off",
// "no-cond-assign": "off",
// "no-redeclare": "off",
// "no-unreachable": "off",
// "no-nested-ternary": "off",
// "operator-linebreak": "off",
// "guard-for-in": "off",
// "no-unneeded-ternary": "off",
// "no-sequences": "off",
// "no-extend-native": "off",
// "no-shadow-restricted-names": "off",
// "no-extra-boolean-cast": "off",
// "no-path-concat": "off",
// "no-unused-expressions": "off",
// "no-return-assign": "off",
// "no-restricted-modules": "off",
// "object-curly-spacing": "off",
// "indent": "off",
// "padded-blocks": "off",
// "eol-last": "off",
// "lines-around-directive": "off",
// "strict": "off",
// "comma-dangle": "off",
// "no-multi-spaces": "off",
// "quotes": "off",
// "keyword-spacing": "off",
// "no-mixed-operators": "off",
// "comma-spacing": "off",
// "no-trailing-spaces": "off",
// "key-spacing": "off",
// "no-multiple-empty-lines": "off",
// "spaced-comment": "off",
// "space-in-parens": "off",
// "block-spacing": "off",
// "quote-props": "off",
// "space-unary-ops": "off",
// "no-empty": "off",
// "dot-notation": "off",
// "func-call-spacing": "off",
// "array-bracket-spacing": "off",
// "object-property-newline": "off",
// "no-continue": "off",
// "no-extra-semi": "off",
// "no-spaced-func": "off",
// "no-useless-return": "off"
}
}

6
.gitignore vendored
View File

@ -216,9 +216,3 @@ pip-log.txt
sftp-config.json sftp-config.json
node_modules/ node_modules/
####################
# JetBrains
####################
.idea

View File

@ -14,6 +14,4 @@ NodeBB Plugin that allows users to login/register via any configured OAuth provi
## Trouble? ## Trouble?
The NodeBB team builds out SSO plugins for a nominal fee. [Reach out to us for a quote.](mailto:sales@nodebb.org)
Find us on [the community forums](http://community.nodebb.org)! Find us on [the community forums](http://community.nodebb.org)!

View File

@ -1,3 +0,0 @@
'use strict';
module.exports = { extends: ['@commitlint/config-angular'] };

View File

@ -1,6 +1,6 @@
'use strict'; (function(module) {
"use strict";
(function (module) {
/* /*
Welcome to the SSO OAuth plugin! If you're inspecting this code, you're probably looking to Welcome to the SSO OAuth plugin! If you're inspecting this code, you're probably looking to
hook up NodeBB with your existing OAuth endpoint. hook up NodeBB with your existing OAuth endpoint.
@ -11,65 +11,42 @@
Step 2: Give it a whirl. If you see the congrats message, you're doing well so far! Step 2: Give it a whirl. If you see the congrats message, you're doing well so far!
Step 3: Customise the `parseUserReturn` method to normalise your user route's data return into Step 3: Customise the `parseUserReturn` method to normalise your user route's data return into
a format accepted by NodeBB. Instructions are provided there. (Line 146) a format accepted by NodeBB. Instructions are provided there. (Line 137)
Step 4: If all goes well, you'll be able to login/register via your OAuth endpoint credentials. Step 4: If all goes well, you'll be able to login/register via your OAuth endpoint credentials.
*/ */
const User = require.main.require('./src/user'); var User = module.parent.require('./user'),
const Groups = require.main.require('./src/groups'); Groups = module.parent.require('./groups'),
const db = require.main.require('./src/database'); meta = module.parent.require('./meta'),
const authenticationController = require.main.require('./src/controllers/authentication'); db = module.parent.require('../src/database'),
passport = module.parent.require('passport'),
fs = module.parent.require('fs'),
path = module.parent.require('path'),
nconf = module.parent.require('nconf'),
winston = module.parent.require('winston'),
async = module.parent.require('async'),
const async = require('async'); constants = Object.freeze({
type: '', // Either 'oauth' or 'oauth2'
const passport = module.parent.require('passport'); name: '', // Something unique to your OAuth provider in lowercase, like "github", or "nodebb"
const nconf = module.parent.require('nconf'); oauth: {
const winston = module.parent.require('winston'); requestTokenURL: '',
accessTokenURL: '',
/** userAuthorizationURL: '',
* REMEMBER consumerKey: '',
* Never save your OAuth Key/Secret or OAuth2 ID/Secret pair in code! It could be published and leaked accidentally. consumerSecret: ''
* Save it into your config.json file instead: },
* oauth2: {
* { authorizationURL: '',
* ... tokenURL: '',
* "oauth": { clientID: '',
* "id": "someoauthid", clientSecret: ''
* "secret": "youroauthsecret" },
* } userRoute: '' // This is the address to your app's "user profile" API endpoint (expects JSON)
* ... }),
* } configOk = false,
* OAuth = {}, passportOAuth, opts;
* ... or use environment variables instead:
*
* `OAUTH__ID=someoauthid OAUTH__SECRET=youroauthsecret node app.js`
*/
const constants = Object.freeze({
type: 'oauth2', // Either 'oauth' or 'oauth2'
name: 'icynet', // Something unique to your OAuth provider in lowercase, like "github", or "nodebb"
scope: 'email image privileges',
oauth: {
requestTokenURL: '',
accessTokenURL: '',
userAuthorizationURL: '',
consumerKey: nconf.get('oauth:key'), // don't change this line
consumerSecret: nconf.get('oauth:secret'), // don't change this line
},
oauth2: {
authorizationURL: 'https://secure.icynet.eu/oauth2/authorize',
tokenURL: 'https://secure.icynet.eu/oauth2/token',
clientID: nconf.get('oauth:id'),
clientSecret: nconf.get('oauth:secret'),
},
userRoute: 'https://api.icynet.eu/api/user'
})
const OAuth = {};
let configOk = false;
let passportOAuth;
let opts;
if (!constants.name) { if (!constants.name) {
winston.error('[sso-oauth] Please specify a name for your OAuth provider (library.js:32)'); winston.error('[sso-oauth] Please specify a name for your OAuth provider (library.js:32)');
@ -81,7 +58,7 @@
configOk = true; configOk = true;
} }
OAuth.getStrategy = function (strategies, callback) { OAuth.getStrategy = function(strategies, callback) {
if (configOk) { if (configOk) {
passportOAuth = require('passport-oauth')[constants.type === 'oauth' ? 'OAuthStrategy' : 'OAuth2Strategy']; passportOAuth = require('passport-oauth')[constants.type === 'oauth' ? 'OAuthStrategy' : 'OAuth2Strategy'];
@ -90,21 +67,18 @@
opts = constants.oauth; opts = constants.oauth;
opts.callbackURL = nconf.get('url') + '/auth/' + constants.name + '/callback'; opts.callbackURL = nconf.get('url') + '/auth/' + constants.name + '/callback';
passportOAuth.Strategy.prototype.userProfile = function (token, secret, params, done) { passportOAuth.Strategy.prototype.userProfile = function(token, secret, params, done) {
this._oauth.get(constants.userRoute, token, secret, function (err, body/* , res */) { this._oauth.get(constants.userRoute, token, secret, function(err, body, res) {
if (err) { if (err) { return done(new InternalOAuthError('failed to fetch user profile', err)); }
return done(err);
}
try { try {
var json = JSON.parse(body); var json = JSON.parse(body);
OAuth.parseUserReturn(json, function (err, profile) { OAuth.parseUserReturn(json, function(err, profile) {
if (err) return done(err); if (err) return done(err);
profile.provider = constants.name; profile.provider = constants.name;
done(null, profile); done(null, profile);
}); });
} catch (e) { } catch(e) {
done(e); done(e);
} }
}); });
@ -114,42 +88,34 @@
opts = constants.oauth2; opts = constants.oauth2;
opts.callbackURL = nconf.get('url') + '/auth/' + constants.name + '/callback'; opts.callbackURL = nconf.get('url') + '/auth/' + constants.name + '/callback';
passportOAuth.Strategy.prototype.userProfile = function (accessToken, done) { passportOAuth.Strategy.prototype.userProfile = function(accessToken, done) {
this._oauth2.get(constants.userRoute, accessToken, function (err, body/* , res */) { this._oauth2.get(constants.userRoute, accessToken, function(err, body, res) {
if (err) { if (err) { return done(new InternalOAuthError('failed to fetch user profile', err)); }
return done(err);
}
try { try {
var json = JSON.parse(body); var json = JSON.parse(body);
OAuth.parseUserReturn(json, function (err, profile) { OAuth.parseUserReturn(json, function(err, profile) {
if (err) return done(err); if (err) return done(err);
profile.provider = constants.name; profile.provider = constants.name;
done(null, profile); done(null, profile);
}); });
} catch (e) { } catch(e) {
done(e); done(e);
} }
}); });
}; };
} }
opts.passReqToCallback = true; passport.use(constants.name, new passportOAuth(opts, function(token, secret, profile, done) {
passport.use(constants.name, new passportOAuth(opts, function (req, token, secret, profile, done) {
OAuth.login({ OAuth.login({
oAuthid: profile.id, oAuthid: profile.id,
handle: profile.displayName, handle: profile.displayName,
email: profile.emails[0].value, email: profile.emails[0].value,
picture: profile.picture,
isAdmin: profile.isAdmin isAdmin: profile.isAdmin
}, function(err, user) { }, function(err, user) {
if (err) { if (err) {
return done(err); return done(err);
} }
authenticationController.onSuccessfulLogin(req, user.uid);
done(null, user); done(null, user);
}); });
})); }));
@ -159,7 +125,7 @@
url: '/auth/' + constants.name, url: '/auth/' + constants.name,
callbackURL: '/auth/' + constants.name + '/callback', callbackURL: '/auth/' + constants.name + '/callback',
icon: 'fa-check-square', icon: 'fa-check-square',
scope: (constants.scope || '').split(','), scope: (constants.scope || '').split(',')
}); });
callback(null, strategies); callback(null, strategies);
@ -168,59 +134,69 @@
} }
}; };
OAuth.parseUserReturn = function (data, callback) { OAuth.parseUserReturn = function(data, callback) {
// Alter this section to include whatever data is necessary
// NodeBB *requires* the following: id, displayName, emails.
// Everything else is optional.
// Find out what is available by uncommenting this line:
// console.log(data);
var profile = {}; var profile = {};
profile.id = data.uuid; profile.id = data.id;
profile.displayName = data.display_name; profile.displayName = data.name;
profile.emails = [{ value: data.email }]; profile.emails = [{ value: data.email }];
profile.isAdmin = (data.privileges || []).includes('admin');
profile.picture = data.image;
// eslint-disable-next-line // Do you want to automatically make somebody an admin? This line might help you do that...
// profile.isAdmin = data.isAdmin ? true : false;
// Delete or comment out the next TWO (2) lines when you are ready to proceed
process.stdout.write('===\nAt this point, you\'ll need to customise the above section to id, displayName, and emails into the "profile" object.\n===');
return callback(new Error('Congrats! So far so good -- please see server log for details'));
callback(null, profile); callback(null, profile);
}; }
OAuth.login = function (payload, callback) { OAuth.login = function(payload, callback) {
OAuth.getUidByOAuthid(payload.oAuthid, function (err, uid) { OAuth.getUidByOAuthid(payload.oAuthid, function(err, uid) {
if (err) { if(err) {
return callback(err); return callback(err);
} }
if (uid !== null) { if (uid !== null) {
// Existing User // Existing User
callback(null, { callback(null, {
uid: uid, uid: uid
}); });
} else { } else {
// New User // New User
var success = function (uid) { var success = function(uid) {
// Save provider-specific information to the user // Save provider-specific information to the user
User.setUserField(uid, constants.name + 'Id', payload.oAuthid); User.setUserField(uid, constants.name + 'Id', payload.oAuthid);
db.setObjectField(constants.name + 'Id:uid', payload.oAuthid, uid); db.setObjectField(constants.name + 'Id:uid', payload.oAuthid, uid);
if (payload.isAdmin) { if (payload.isAdmin) {
Groups.join('administrators', uid, function (err) { Groups.join('administrators', uid, function(err) {
callback(err, { callback(null, {
uid: uid, uid: uid
}); });
}); });
} else { } else {
callback(null, { callback(null, {
uid: uid, uid: uid
}); });
} }
}; };
User.getUidByEmail(payload.email, function (err, uid) { User.getUidByEmail(payload.email, function(err, uid) {
if (err) { if(err) {
return callback(err); return callback(err);
} }
if (!uid) { if (!uid) {
User.create({ User.create({
username: payload.handle, username: payload.handle,
email: payload.email, email: payload.email
picture: payload.picture
}, function(err, uid) { }, function(err, uid) {
if(err) { if(err) {
return callback(err); return callback(err);
@ -236,8 +212,8 @@
}); });
}; };
OAuth.getUidByOAuthid = function (oAuthid, callback) { OAuth.getUidByOAuthid = function(oAuthid, callback) {
db.getObjectField(constants.name + 'Id:uid', oAuthid, function (err, uid) { db.getObjectField(constants.name + 'Id:uid', oAuthid, function(err, uid) {
if (err) { if (err) {
return callback(err); return callback(err);
} }
@ -245,27 +221,20 @@
}); });
}; };
OAuth.deleteUserData = function (data, callback) { OAuth.deleteUserData = function(uid, callback) {
async.waterfall([ async.waterfall([
async.apply(User.getUserField, data.uid, constants.name + 'Id'), async.apply(User.getUserField, uid, constants.name + 'Id'),
function (oAuthIdToDelete, next) { function(oAuthIdToDelete, next) {
db.deleteObjectField(constants.name + 'Id:uid', oAuthIdToDelete, next); db.deleteObjectField(constants.name + 'Id:uid', oAuthIdToDelete, next);
}, }
], function (err) { ], function(err) {
if (err) { if (err) {
winston.error('[sso-oauth] Could not remove OAuthId data for uid ' + data.uid + '. Error: ' + err); winston.error('[sso-oauth] Could not remove OAuthId data for uid ' + uid + '. Error: ' + err);
return callback(err); return callback(err);
} }
callback();
callback(null, data);
}); });
}; };
// If this filter is not there, the deleteUserData function will fail when getting the oauthId for deletion.
OAuth.whitelistFields = function (params, callback) {
params.whitelist.push(constants.name + 'Id');
callback(null, params);
};
module.exports = OAuth; module.exports = OAuth;
}(module)); }(module));

View File

@ -1,6 +1,6 @@
{ {
"name": "nodebb-plugin-sso-oauth", "name": "nodebb-plugin-sso-oauth",
"version": "0.3.4", "version": "0.2.0",
"description": "NodeBB Generic OAuth SSO", "description": "NodeBB Generic OAuth SSO",
"main": "library.js", "main": "library.js",
"repository": { "repository": {
@ -11,10 +11,8 @@
"nodebb", "nodebb",
"plugin", "plugin",
"oauth", "oauth",
"oauth2",
"sso", "sso",
"single sign on", "single sign on",
"login",
"registration" "registration"
], ],
"author": { "author": {
@ -28,32 +26,6 @@
"readme": "", "readme": "",
"readmeFilename": "README.md", "readmeFilename": "README.md",
"dependencies": { "dependencies": {
"async": "^2",
"passport-oauth": "~1.0.0" "passport-oauth": "~1.0.0"
},
"nbbpm": {
"compatibility": "^1.0.1",
"index": false
},
"devDependencies": {
"@commitlint/cli": "^8.0.0",
"@commitlint/config-angular": "^7.1.2",
"eslint": "^5.16.0",
"eslint-config-airbnb-base": "^12.1.0",
"eslint-plugin-import": "^2.8.0",
"husky": "^2.4.0",
"lint-staged": "^8.2.0"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"lint-staged": {
"*.js": [
"eslint --fix",
"git add"
]
} }
} }

View File

@ -5,8 +5,9 @@
"url": "https://github.com/julianlam/nodebb-plugin-sso-oauth", "url": "https://github.com/julianlam/nodebb-plugin-sso-oauth",
"library": "./library.js", "library": "./library.js",
"hooks": [ "hooks": [
{ "hook": "static:user.delete", "method": "deleteUserData" }, { "hook": "filter:user.delete", "method": "deleteUserData" },
{ "hook": "filter:user.whitelistFields", "method": "whitelistFields" },
{ "hook": "filter:auth.init", "method": "getStrategy" } { "hook": "filter:auth.init", "method": "getStrategy" }
] ],
"templates": "./templates",
"minver": "0.5.0"
} }

View File

@ -0,0 +1,81 @@
<h1><i class="fa fa-key"></i> Generic OAuth Authentication</h1>
<hr />
<form class="sso-oauth-settings">
<div class="alert alert-warning">
<p>
Please refer to your OAuth provider&apos;s documentation for appropriate values. All fields are mandatory.
</p>
<br />
<select name="oauth:type" title="OAuth Strategy" class="form-control">
<option value="x">Disabled</option>
<option value="1">OAuth</option>
<option value="2">OAuth2</option>
</select>
<hr />
<div class="form-group">
<input type="text" data-strategy="1" name="oauth:key" title="OAuth Key" class="form-control input-lg" placeholder="OAuth Key">
</div>
<div class="form-group">
<input type="text" data-strategy="1" name="oauth:secret" title="OAuth Secret" class="form-control" placeholder="OAuth Secret">
</div>
<div class="form-group">
<input type="text" data-strategy="1" name="oauth:reqTokenUrl" title="Token Request URL" class="form-control" placeholder="Token Request URL">
</div>
<div class="form-group">
<input type="text" data-strategy="1" name="oauth:accessTokenUrl" title="Access Token URL" class="form-control" placeholder="Access Token URL">
</div>
<div class="form-group">
<input type="text" data-strategy="1" name="oauth:authUrl" title="Authorization URL" class="form-control" placeholder="Authorization URL">
</div>
<div class="form-group">
<input type="text" data-strategy="2" name="oauth2:id" title="Client ID" class="form-control input-lg" placeholder="Client ID">
</div>
<div class="form-group">
<input type="text" data-strategy="2" name="oauth2:secret" title="Client Secret" class="form-control" placeholder="Client Secret">
</div>
<div class="form-group">
<input type="text" data-strategy="2" name="oauth2:authUrl" title="Authorization URL" class="form-control" placeholder="Authorization URL">
</div>
<div class="form-group">
<input type="text" data-strategy="2" name="oauth2:tokenUrl" title="Token URL" class="form-control" placeholder="Token URL">
</div>
<div class="form-group">
<input type="text" name="oauth:userProfileUrl" title="User Profile URL" class="form-control" placeholder="User Profile URL">
</div>
</div>
</form>
<button class="btn btn-lg btn-primary" type="button" id="save">Save</button>
<script>
require(['settings'], function(Settings) {
Settings.load('sso-oauth', $('.sso-oauth-settings'), function() {
var OAuthType = $('[name="oauth:type"]').val();
toggleFields(OAuthType);
});
$('#save').on('click', function() {
Settings.save('sso-oauth', $('.sso-oauth-settings'));
});
});
var toggleFields = function(value) {
if (value === '1') {
$('[data-strategy="2"]').hide();
$('[data-strategy="1"]').show();
} else if (value === '2') {
$('[data-strategy="1"]').hide();
$('[data-strategy="2"]').show();
} else {
$('[data-strategy]').hide();
}
}
toggleFields(false);
$('[name="oauth:type"]').on('change', function() {
toggleFields(this.value);
})
</script>

3070
yarn.lock

File diff suppressed because it is too large Load Diff